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 path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { stripHiddenUnicode } from "../core/unicode-safety.js";
|
||||
|
||||
const PAI_ROOT = path.join(os.homedir(), ".claude", "PAI");
|
||||
|
||||
|
|
@ -55,7 +56,7 @@ export class PaiConnector {
|
|||
return {
|
||||
path: full,
|
||||
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"));
|
||||
}).map((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);
|
||||
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"));
|
||||
}).map((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);
|
||||
return { name: f, path: skillPath, description: desc };
|
||||
});
|
||||
|
|
@ -107,7 +108,7 @@ export class PaiConnector {
|
|||
if (isaFiles.length === 0) return null;
|
||||
|
||||
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);
|
||||
|
||||
return { path: latest.path, name: latest.name, content, sections };
|
||||
|
|
@ -116,7 +117,7 @@ export class PaiConnector {
|
|||
readFile(relativePath: string): string | null {
|
||||
const full = path.join(this.paiRoot, relativePath);
|
||||
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> {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import * as path from "node:path";
|
|||
import * as os from "node:os";
|
||||
import type { SkillDefinition, SkillStep } from "../core/types.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");
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ export class SkillSync {
|
|||
const skillMd = path.join(this.paiSkillsDir, entry.name, "SKILL.md");
|
||||
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 steps = this.extractSteps(content);
|
||||
const tags = this.extractTags(content);
|
||||
|
|
@ -118,7 +119,7 @@ export class SkillSync {
|
|||
const skillPath = path.join(this.paiSkillsDir, skillName, "SKILL.md");
|
||||
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
|
||||
const lessonsHeader = "## Lessons Learned";
|
||||
|
|
|
|||
Loading…
Reference in New Issue