Skip to content

Commit 18c18ed

Browse files
fix(skills): preserve unsupported YAML frontmatter
1 parent 6d15304 commit 18c18ed

3 files changed

Lines changed: 90 additions & 4 deletions

File tree

src/chrome/src/agent/skills.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ function parseAgentSkillScalar(value) {
111111
}
112112

113113
function parseAgentSkillFrontmatter(content) {
114-
const text = String(content || '').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
114+
const text = String(content || '').replace(/\r\n?/g, '\n').trimStart();
115115
const match = text.match(/^---\n([\s\S]{0,8192}?)\n---(?:\n|$)/);
116116
if (!match) return null;
117117

@@ -129,7 +129,11 @@ function parseAgentSkillFrontmatter(content) {
129129
seen.add(key);
130130

131131
const rawValue = field[2] || '';
132-
if (/^[>|][+-]?$/.test(rawValue.trim())) {
132+
const scalarIndicator = rawValue.trim();
133+
if (/^[>|]/.test(scalarIndicator) && !/^[>|][+-]?$/.test(scalarIndicator)) {
134+
return null;
135+
}
136+
if (/^[>|][+-]?$/.test(scalarIndicator)) {
133137
const block = [];
134138
while (index + 1 < lines.length) {
135139
const next = lines[index + 1];
@@ -144,6 +148,12 @@ function parseAgentSkillFrontmatter(content) {
144148
values[key] = cleanText(block.map((item) => item.slice(indent)).join('\n'));
145149
} else {
146150
values[key] = parseAgentSkillScalar(rawValue);
151+
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
152+
const next = lines[nextIndex];
153+
if (!next.trim() || /^\s*#/.test(next)) continue;
154+
if (/^\s/.test(next)) return null;
155+
break;
156+
}
147157
}
148158
}
149159

src/firefox/src/agent/skills.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ function parseAgentSkillScalar(value) {
111111
}
112112

113113
function parseAgentSkillFrontmatter(content) {
114-
const text = String(content || '').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
114+
const text = String(content || '').replace(/\r\n?/g, '\n').trimStart();
115115
const match = text.match(/^---\n([\s\S]{0,8192}?)\n---(?:\n|$)/);
116116
if (!match) return null;
117117

@@ -129,7 +129,11 @@ function parseAgentSkillFrontmatter(content) {
129129
seen.add(key);
130130

131131
const rawValue = field[2] || '';
132-
if (/^[>|][+-]?$/.test(rawValue.trim())) {
132+
const scalarIndicator = rawValue.trim();
133+
if (/^[>|]/.test(scalarIndicator) && !/^[>|][+-]?$/.test(scalarIndicator)) {
134+
return null;
135+
}
136+
if (/^[>|][+-]?$/.test(scalarIndicator)) {
133137
const block = [];
134138
while (index + 1 < lines.length) {
135139
const next = lines[index + 1];
@@ -144,6 +148,12 @@ function parseAgentSkillFrontmatter(content) {
144148
values[key] = cleanText(block.map((item) => item.slice(indent)).join('\n'));
145149
} else {
146150
values[key] = parseAgentSkillScalar(rawValue);
151+
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
152+
const next = lines[nextIndex];
153+
if (!next.trim() || /^\s*#/.test(next)) continue;
154+
if (/^\s/.test(next)) return null;
155+
break;
156+
}
147157
}
148158
}
149159

test/run.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,7 @@ const {
784784
removeRetiredPackagedSkills: removeRetiredPackagedSkillsCh,
785785
refreshBuiltInSkillRecord: refreshBuiltInSkillRecordCh,
786786
readSkillImportText: readSkillImportTextCh,
787+
stripSkillToolBlocks: stripSkillToolBlocksCh,
787788
buildCustomSkillsPrompt: buildCustomSkillsPromptCh,
788789
getEligibleSkillCatalog: getEligibleSkillCatalogCh,
789790
buildSkillLoaderDefinition: buildSkillLoaderDefinitionCh,
@@ -808,6 +809,7 @@ const {
808809
removeRetiredPackagedSkills: removeRetiredPackagedSkillsFx,
809810
refreshBuiltInSkillRecord: refreshBuiltInSkillRecordFx,
810811
readSkillImportText: readSkillImportTextFx,
812+
stripSkillToolBlocks: stripSkillToolBlocksFx,
811813
buildCustomSkillsPrompt: buildCustomSkillsPromptFx,
812814
getEligibleSkillCatalog: getEligibleSkillCatalogFx,
813815
buildSkillLoaderDefinition: buildSkillLoaderDefinitionFx,
@@ -12821,6 +12823,70 @@ Keep this text unchanged.`;
1282112823
}
1282212824
});
1282312825

12826+
test('Agent Skills parsing rejects unsupported YAML without data loss and normalizes raw input', () => {
12827+
const wrappedPlain = `---
12828+
name: pdf-processing
12829+
description: Extract PDF text and fill forms.
12830+
Use when working with PDF documents.
12831+
---
12832+
# Safe fallback
12833+
12834+
Keep the complete source.`;
12835+
const explicitIndent = `---
12836+
name: pdf-processing
12837+
description: >2-
12838+
Extract PDF text and fill forms.
12839+
---
12840+
# Safe fallback
12841+
12842+
Keep the explicit block source.`;
12843+
const supportedRaw = '\uFEFF\r\n\r\n---\r\nname: pdf-processing\r\ndescription: Extract PDF text.\r\n---\r\n# PDF workflow\r\n\r\nFollow it.';
12844+
const oversizedFrontmatter = `---
12845+
name: pdf-processing
12846+
description: Extract PDF text.
12847+
metadata: ${'x'.repeat(8200)}
12848+
---
12849+
# Safe fallback
12850+
12851+
Keep the oversized source.`;
12852+
12853+
for (const [label, normalizeSkills, buildPrompt, stripBlocks] of [
12854+
['chrome', normalizeCustomSkillsCh, buildCustomSkillsPromptCh, stripSkillToolBlocksCh],
12855+
['firefox', normalizeCustomSkillsFx, buildCustomSkillsPromptFx, stripSkillToolBlocksFx],
12856+
]) {
12857+
for (const [caseName, content, preservedText] of [
12858+
['wrapped plain scalar', wrappedPlain, 'Use when working with PDF documents.'],
12859+
['explicit indentation indicator', explicitIndent, 'description: >2-'],
12860+
]) {
12861+
const id = `unsupported-${caseName.replace(/\s+/g, '-')}`;
12862+
const [skill] = normalizeSkills([{ id, content }]);
12863+
assert.equal(skill.name, 'Safe fallback', `${label}: ${caseName} was partially accepted`);
12864+
const prompt = buildPrompt([skill], {
12865+
mode: 'act',
12866+
tier: 'full',
12867+
activeSkillIds: new Set([id]),
12868+
});
12869+
assert.match(prompt, new RegExp(preservedText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), `${label}: ${caseName} source was lost`);
12870+
assert.match(prompt, /Keep the .* source\./, `${label}: ${caseName} body was lost`);
12871+
}
12872+
12873+
const stripped = stripBlocks(supportedRaw);
12874+
assert.equal(
12875+
stripped,
12876+
'# PDF workflow\n\nFollow it.',
12877+
`${label}: raw BOM/CRLF/leading whitespace changed frontmatter handling`,
12878+
);
12879+
12880+
const [oversized] = normalizeSkills([{ id: 'oversized-frontmatter', content: oversizedFrontmatter }]);
12881+
assert.equal(oversized.name, 'Safe fallback', `${label}: oversized frontmatter was partially accepted`);
12882+
assert.match(
12883+
stripBlocks(oversizedFrontmatter),
12884+
/metadata: x{100}/,
12885+
`${label}: oversized frontmatter was destructively stripped`,
12886+
);
12887+
}
12888+
});
12889+
1282412890
test('skill semantic intents are bounded, explicit, and shared by loader catalogs', () => {
1282512891
for (const [label, normalizeSkills, getCatalog, buildLoader, maxIntents, maxIntentChars] of [
1282612892
['chrome', normalizeCustomSkillsCh, getEligibleSkillCatalogCh, buildSkillLoaderDefinitionCh, MAX_CUSTOM_SKILL_INTENTS_CH, MAX_CUSTOM_SKILL_INTENT_CHARS_CH],

0 commit comments

Comments
 (0)