fix: strip hidden unicode from imported context
This commit is contained in:
parent
5a38f57db7
commit
4499b860be
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { hasHiddenUnicode, stripHiddenUnicode } from "./unicode-safety.js";
|
||||||
|
|
||||||
|
describe("unicode safety", () => {
|
||||||
|
it("strips hidden instruction carriers", () => {
|
||||||
|
const tag = String.fromCodePoint(0xe0069);
|
||||||
|
const text = `safe${tag}\u200b\u202etext`;
|
||||||
|
|
||||||
|
expect(hasHiddenUnicode(text)).toBe(true);
|
||||||
|
expect(stripHiddenUnicode(text)).toBe("safetext");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
const HIDDEN_UNICODE = /[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFE00-\uFE0F\u{E0000}-\u{E007F}]/gu;
|
||||||
|
|
||||||
|
export function stripHiddenUnicode(text: string): string {
|
||||||
|
// ponytail: strips known invisible instruction carriers; add reporting metadata if UI needs forensics.
|
||||||
|
return text.replace(HIDDEN_UNICODE, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasHiddenUnicode(text: string): boolean {
|
||||||
|
HIDDEN_UNICODE.lastIndex = 0;
|
||||||
|
return HIDDEN_UNICODE.test(text);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
|
import { stripHiddenUnicode } from "../core/unicode-safety.js";
|
||||||
|
|
||||||
const PAI_ROOT = path.join(os.homedir(), ".claude", "PAI");
|
const PAI_ROOT = path.join(os.homedir(), ".claude", "PAI");
|
||||||
|
|
||||||
|
|
@ -55,7 +56,7 @@ export class PaiConnector {
|
||||||
return {
|
return {
|
||||||
path: full,
|
path: full,
|
||||||
name: f.replace(/\.md$/, ""),
|
name: f.replace(/\.md$/, ""),
|
||||||
content: fs.readFileSync(full, "utf-8"),
|
content: stripHiddenUnicode(fs.readFileSync(full, "utf-8")),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +71,7 @@ export class PaiConnector {
|
||||||
return fs.statSync(skillPath).isDirectory() && fs.existsSync(path.join(skillPath, "SKILL.md"));
|
return fs.statSync(skillPath).isDirectory() && fs.existsSync(path.join(skillPath, "SKILL.md"));
|
||||||
}).map((f) => {
|
}).map((f) => {
|
||||||
const skillPath = path.join(legacyDir, f);
|
const skillPath = path.join(legacyDir, f);
|
||||||
const skillMd = fs.readFileSync(path.join(skillPath, "SKILL.md"), "utf-8");
|
const skillMd = stripHiddenUnicode(fs.readFileSync(path.join(skillPath, "SKILL.md"), "utf-8"));
|
||||||
const desc = skillMd.split("\n").slice(0, 5).join(" ").slice(0, 200);
|
const desc = skillMd.split("\n").slice(0, 5).join(" ").slice(0, 200);
|
||||||
return { name: f, path: skillPath, description: desc };
|
return { name: f, path: skillPath, description: desc };
|
||||||
});
|
});
|
||||||
|
|
@ -81,7 +82,7 @@ export class PaiConnector {
|
||||||
return fs.statSync(skillPath).isDirectory() && fs.existsSync(path.join(skillPath, "SKILL.md"));
|
return fs.statSync(skillPath).isDirectory() && fs.existsSync(path.join(skillPath, "SKILL.md"));
|
||||||
}).map((f) => {
|
}).map((f) => {
|
||||||
const skillPath = path.join(skillsDir, f);
|
const skillPath = path.join(skillsDir, f);
|
||||||
const skillMd = fs.readFileSync(path.join(skillPath, "SKILL.md"), "utf-8");
|
const skillMd = stripHiddenUnicode(fs.readFileSync(path.join(skillPath, "SKILL.md"), "utf-8"));
|
||||||
const desc = skillMd.split("\n").slice(0, 5).join(" ").slice(0, 200);
|
const desc = skillMd.split("\n").slice(0, 5).join(" ").slice(0, 200);
|
||||||
return { name: f, path: skillPath, description: desc };
|
return { name: f, path: skillPath, description: desc };
|
||||||
});
|
});
|
||||||
|
|
@ -107,7 +108,7 @@ export class PaiConnector {
|
||||||
if (isaFiles.length === 0) return null;
|
if (isaFiles.length === 0) return null;
|
||||||
|
|
||||||
const latest = isaFiles.sort((a, b) => b.name.localeCompare(a.name))[0];
|
const latest = isaFiles.sort((a, b) => b.name.localeCompare(a.name))[0];
|
||||||
const content = fs.readFileSync(latest.path, "utf-8");
|
const content = stripHiddenUnicode(fs.readFileSync(latest.path, "utf-8"));
|
||||||
const sections = this.parseSections(content);
|
const sections = this.parseSections(content);
|
||||||
|
|
||||||
return { path: latest.path, name: latest.name, content, sections };
|
return { path: latest.path, name: latest.name, content, sections };
|
||||||
|
|
@ -116,7 +117,7 @@ export class PaiConnector {
|
||||||
readFile(relativePath: string): string | null {
|
readFile(relativePath: string): string | null {
|
||||||
const full = path.join(this.paiRoot, relativePath);
|
const full = path.join(this.paiRoot, relativePath);
|
||||||
if (!fs.existsSync(full)) return null;
|
if (!fs.existsSync(full)) return null;
|
||||||
return fs.readFileSync(full, "utf-8");
|
return stripHiddenUnicode(fs.readFileSync(full, "utf-8"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseSections(content: string): Map<string, string> {
|
private parseSections(content: string): Map<string, string> {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import * as path from "node:path";
|
||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import type { SkillDefinition, SkillStep } from "../core/types.js";
|
import type { SkillDefinition, SkillStep } from "../core/types.js";
|
||||||
import { StateStore } from "../core/state-store.js";
|
import { StateStore } from "../core/state-store.js";
|
||||||
|
import { stripHiddenUnicode } from "../core/unicode-safety.js";
|
||||||
|
|
||||||
const PAI_SKILLS_DIR = path.join(os.homedir(), ".claude", "skills");
|
const PAI_SKILLS_DIR = path.join(os.homedir(), ".claude", "skills");
|
||||||
|
|
||||||
|
|
@ -31,7 +32,7 @@ export class SkillSync {
|
||||||
const skillMd = path.join(this.paiSkillsDir, entry.name, "SKILL.md");
|
const skillMd = path.join(this.paiSkillsDir, entry.name, "SKILL.md");
|
||||||
if (!fs.existsSync(skillMd)) continue;
|
if (!fs.existsSync(skillMd)) continue;
|
||||||
|
|
||||||
const content = fs.readFileSync(skillMd, "utf-8");
|
const content = stripHiddenUnicode(fs.readFileSync(skillMd, "utf-8"));
|
||||||
const description = this.extractDescription(content);
|
const description = this.extractDescription(content);
|
||||||
const steps = this.extractSteps(content);
|
const steps = this.extractSteps(content);
|
||||||
const tags = this.extractTags(content);
|
const tags = this.extractTags(content);
|
||||||
|
|
@ -118,7 +119,7 @@ export class SkillSync {
|
||||||
const skillPath = path.join(this.paiSkillsDir, skillName, "SKILL.md");
|
const skillPath = path.join(this.paiSkillsDir, skillName, "SKILL.md");
|
||||||
if (!fs.existsSync(skillPath)) return null;
|
if (!fs.existsSync(skillPath)) return null;
|
||||||
|
|
||||||
let content = fs.readFileSync(skillPath, "utf-8");
|
let content = stripHiddenUnicode(fs.readFileSync(skillPath, "utf-8"));
|
||||||
|
|
||||||
// Find or create a Lessons Learned section
|
// Find or create a Lessons Learned section
|
||||||
const lessonsHeader = "## Lessons Learned";
|
const lessonsHeader = "## Lessons Learned";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue