Skip to content

Commit 3035bcf

Browse files
authored
Merge pull request #517 from alectimison-maker/feat/agent-skills-frontmatter
feat(skills): support Agent Skills frontmatter
2 parents 10a215c + 0f0c465 commit 3035bcf

9 files changed

Lines changed: 748 additions & 16 deletions

File tree

docs/architecture.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,18 +298,23 @@ new default IDs can still be migrated into existing installations.
298298
intents (40 characters each). Intents are cross-language meaning hints for
299299
the LLM, not literal keywords. Without metadata, the first prose paragraph
300300
becomes the summary, intents stay empty, and the skill defaults to Act/Dev.
301+
An imported Agent Skills `SKILL.md` can instead supply its standard `name`
302+
and `description` frontmatter for the name and summary; an explicit Settings
303+
name and `webbrain-skill` metadata retain precedence.
301304
`getEligibleSkillCatalog()` produces the shared `{id,name,summary,intents}`
302305
records used by both the planner and `load_skill({skill_id})`. Ask sees only
303306
explicitly Ask-compatible skills, while Compact has no skill surface.
304307
- Prompt instructions: `buildCustomSkillsPrompt()` strips both metadata and
305-
`webbrain-tools` fences, then appends full prose only for skills activated on
306-
the current run. Active IDs reset before the next user turn. Trusted
308+
`webbrain-tools` fences, strips valid Agent Skills frontmatter, then appends
309+
full prose only for skills activated on the current run. Active IDs reset
310+
before the next user turn. Trusted
307311
recommended actions can preactivate the skill that owns their first tool;
308312
NYTimes adapter runs narrowly preactivate FreeSkillz so its site-scoped,
309313
read-only article fallback is ready after a structured blocking `pageGate`.
310314
- Tool exposure: `buildSkillToolDefinitions()` reads manifests only from active
311-
skills and appends compatible schemas to `getToolsForMode(...)` at LLM-call
312-
time, respecting mode, tier, and site adapter. Download-job tools remain
315+
skills' Markdown bodies (never from Agent Skills frontmatter) and appends
316+
compatible schemas to `getToolsForMode(...)` at LLM-call time, respecting
317+
mode, tier, and site adapter. Download-job tools remain
313318
hidden in Ask and require their normal permission gate in action modes.
314319

315320
Loading is idempotent and multiple relevant skills can be active in one run.

docs/fr/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ while (steps < maxSteps) {
200200

201201
Paramètres -> Compétences stocke les compétences activées dans `customSkills` (`chrome.storage.local` ou `browser.storage.local`). Au démarrage, `background.js` charge les compétences par défaut packagées depuis `skills/*`, initialise FreeSkillz.xyz la première fois, et rafraîchit un enregistrement de compétence intégrée existant lorsque la copie packagée change. Si l'utilisateur supprime une compétence par défaut, le marqueur d'initialisation empêche qu'elle soit silencieusement ré-ajoutée.
202202

203-
`agent/skills.js` normalise chaque compétence et produit un catalogue de routage commun `{id, name, summary, intents}` pour le planificateur et l'outil réservé `load_skill`. Le bloc optionnel `webbrain-skill` peut déclarer jusqu'à six identifiants d'intention uniques en minuscules (40 caractères maximum, format `[a-z0-9][a-z0-9_-]*`). Ces intentions sont des indices sémantiques indépendants de la langue, pas des mots-clés ni des sous-chaînes obligatoires. Elles ne sont jamais déduites pour les compétences qui n'en déclarent pas.
203+
`agent/skills.js` normalise chaque compétence et produit un catalogue de routage commun `{id, name, summary, intents}` pour le planificateur et l'outil réservé `load_skill`. Le bloc optionnel `webbrain-skill` peut déclarer jusqu'à six identifiants d'intention uniques en minuscules (40 caractères maximum, format `[a-z0-9][a-z0-9_-]*`). Ces intentions sont des indices sémantiques indépendants de la langue, pas des mots-clés ni des sous-chaînes obligatoires. Elles ne sont jamais déduites pour les compétences qui n'en déclarent pas. Un fichier Agent Skills `SKILL.md` importé peut fournir son `name` et sa `description` standard pour le nom et le résumé ; le nom saisi dans Paramètres et les métadonnées `webbrain-skill` restent prioritaires. Les métadonnées Agent Skills valides sont retirées avant le chargement du corps Markdown. Les manifestes `webbrain-skill` et `webbrain-tools` ne sont lus que dans ce corps, jamais dans les métadonnées Agent Skills.
204204

205205
Chaque exécution commence sans instructions complètes ni outils de compétence. Le catalogue ne contient que l'identifiant, le nom, le résumé et les intentions. Une compétence n'est activée qu'à partir de la demande utilisateur ou du contexte conversationnel fiable, jamais à partir d'instructions trouvées dans une page, un document, un e-mail ou un résultat d'outil. Ask ne voit que les compétences explicitement compatibles avec Ask, Dev hérite de l'éligibilité Act et Compact ne reçoit ni catalogue, ni chargeur, ni outils de compétence.
206206

docs/fr/skills.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@ correspondance littérale de mots-clés. Les compétences sans métadonnées
3232
déduisent leur résumé du premier paragraphe de prose, n'ont aucune intention
3333
déduite, et utilisent Act/Dev par défaut.
3434

35+
WebBrain reconnaît également les métadonnées YAML obligatoires `name` et
36+
`description` d'un fichier
37+
[Agent Skills `SKILL.md`](https://agentskills.io/specification) importé. Le nom
38+
et la description alimentent le catalogue de routage, et les métadonnées sont
39+
retirées avant le chargement du corps Markdown. Un nom saisi dans Paramètres et
40+
un bloc `webbrain-skill` restent prioritaires.
41+
42+
Cette compatibilité concerne uniquement les instructions. WebBrain importe un
43+
seul document texte ; il ne récupère pas les répertoires `scripts/`,
44+
`references/` ou `assets/`, n'exécute pas le code de la compétence et ne traite
45+
pas le champ Agent Skills `allowed-tools` comme une permission ou un manifeste
46+
d'outils WebBrain. Utilisez `webbrain-tools` pour les outils HTTP WebBrain.
47+
WebBrain ne reconnaît les blocs `webbrain-skill` et `webbrain-tools` que dans
48+
le corps Markdown après des métadonnées valides ; du texte ressemblant à un
49+
bloc dans les métadonnées ne peut ni autoriser le routage ni enregistrer
50+
d'outil.
51+
3552
## Outils de compétence
3653

3754
Une compétence peut exposer des outils HTTP en lecture seule, ou des outils de

docs/skills.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ Intents are cross-language *meaning* hints for the LLM, not literal keyword
2929
matching. Skills without metadata infer the first prose paragraph as their
3030
summary, have no inferred intents, and default to Act/Dev.
3131

32+
WebBrain also recognizes the required `name` and `description` YAML frontmatter
33+
from an imported [Agent Skills `SKILL.md`](https://agentskills.io/specification).
34+
The name and description populate the routing catalog, and the frontmatter is
35+
removed before the Markdown body is loaded. A name entered in Settings and a
36+
`webbrain-skill` block still take precedence.
37+
38+
This is instruction-only compatibility. WebBrain imports one text document; it
39+
does not fetch bundled `scripts/`, `references/`, or `assets/`, execute skill
40+
code, or treat the Agent Skills `allowed-tools` field as a WebBrain permission
41+
or tool manifest. Use `webbrain-tools` for WebBrain HTTP tools. WebBrain
42+
recognizes `webbrain-skill` and `webbrain-tools` fences only in the Markdown
43+
body after valid frontmatter; fence-like text inside frontmatter cannot grant
44+
routing eligibility or register tools.
45+
3246
## Skill tools
3347

3448
A skill can expose read-only HTTP tools, or short-lived download-job tools, with

docs/zh-CN/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ while (steps < maxSteps) {
191191

192192
### 步骤 6a:技能与动态工具暴露
193193

194-
技能(`skills.js`)会规范每个技能,并为规划器和保留工具 `load_skill` 生成同一个 `{id, name, summary, intents}` 路由目录。可选的 `webbrain-skill` 块最多声明 6 个唯一的小写意图标识符,每个不超过 40 个字符,并符合 `[a-z0-9][a-z0-9_-]*`。这些意图是与语言无关的语义路由提示,不是必须逐字匹配的关键词或子字符串;未声明意图的技能不会被自动推断标签。
194+
技能(`skills.js`)会规范每个技能,并为规划器和保留工具 `load_skill` 生成同一个 `{id, name, summary, intents}` 路由目录。可选的 `webbrain-skill` 块最多声明 6 个唯一的小写意图标识符,每个不超过 40 个字符,并符合 `[a-z0-9][a-z0-9_-]*`。这些意图是与语言无关的语义路由提示,不是必须逐字匹配的关键词或子字符串;未声明意图的技能不会被自动推断标签。导入的 Agent Skills `SKILL.md` 可以用标准 `name``description` 提供名称及摘要;设置中输入的名称和 `webbrain-skill` 元数据仍然优先。在加载 Markdown 正文前会移除有效的 Agent Skills frontmatter。`webbrain-skill``webbrain-tools` 清单只从该正文读取,绝不会从 Agent Skills frontmatter 读取。
195195

196196
每次运行开始时都不包含完整技能说明或技能工具。目录只暴露 ID、名称、摘要和意图。技能只能根据用户请求或可信的对话上下文激活,不能根据页面、文档、邮件或工具结果中的指令激活。Ask 只看到明确兼容 Ask 的技能,Dev 继承 Act 的可用性,Compact 则没有目录、加载器或技能工具。
197197

docs/zh-CN/skills.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ Mid 和 Full 运行会收到一个小型的可用技能目录:ID、名称、
2424
意图是给 LLM 的跨语言*语义*提示,而非字面关键词匹配。没有元数据的技能会将第一段
2525
正文推断为摘要,不会推断出意图,并默认为 Act/Dev。
2626

27+
WebBrain 也会识别导入的
28+
[Agent Skills `SKILL.md`](https://agentskills.io/specification) 中必需的
29+
`name``description` YAML frontmatter。名称和描述会写入路由目录,而在加载
30+
Markdown 正文前会移除 frontmatter。在设置中输入的名称及 `webbrain-skill` 块仍有
31+
更高优先级。
32+
33+
这仅属于指令兼容。WebBrain 只导入一个文本文档;它不会获取打包的 `scripts/`
34+
`references/``assets/`,不会执行技能代码,也不会把 Agent Skills 的
35+
`allowed-tools` 字段当作 WebBrain 权限或工具清单。WebBrain HTTP 工具仍应使用
36+
`webbrain-tools`。WebBrain 只识别有效 frontmatter 之后 Markdown 正文中的
37+
`webbrain-skill``webbrain-tools` 围栏;frontmatter 内形似围栏的文本不能授予
38+
路由资格或注册工具。
39+
2740
## 技能工具
2841

2942
技能可以通过围栏 `webbrain-tools` JSON 清单暴露只读 HTTP 工具,或短生命周期的

src/chrome/src/agent/skills.js

Lines changed: 178 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,167 @@ function inferName(content, index) {
9797
return (firstLine || `Skill ${index + 1}`).slice(0, 80);
9898
}
9999

100+
function parseAgentSkillScalar(value) {
101+
const raw = String(value || '').trim();
102+
if (
103+
!raw
104+
|| /^[!&*\[\]{},#|>%@`]/.test(raw)
105+
|| /^[?:-](?:[ \t]|$)/.test(raw)
106+
) return null;
107+
const singleQuoted = raw.match(/^'((?:''|[^'])*)'(?:\s+#.*)?$/);
108+
if (singleQuoted) return singleQuoted[1].replace(/''/g, "'");
109+
const doubleQuoted = raw.match(/^("(?:\\.|[^"\\])*")(?:\s+#.*)?$/);
110+
if (doubleQuoted) {
111+
try {
112+
const parsed = JSON.parse(doubleQuoted[1]);
113+
return typeof parsed === 'string' ? parsed : null;
114+
} catch {
115+
return null;
116+
}
117+
}
118+
if (/^['"]/.test(raw)) return null;
119+
const plain = raw.replace(/\s+#.*$/, '').trim();
120+
if (!plain || /:(?:[ \t]|$)/.test(plain)) return null;
121+
return plain;
122+
}
123+
124+
function agentSkillLineIndent(line) {
125+
const leading = String(line || '').match(/^[ \t]*/)?.[0] || '';
126+
return leading.includes('\t') ? null : leading.length;
127+
}
128+
129+
function parseAgentSkillBlock(lines, startIndex, indicator) {
130+
if (!/^[>|](?:(?:[1-9][+-]?)|(?:[+-][1-9]?))?$/.test(indicator)) return null;
131+
const explicitIndent = Number(indicator.match(/[1-9]/)?.[0] || 0);
132+
const block = [];
133+
let endIndex = startIndex;
134+
let contentIndent = explicitIndent || 0;
135+
136+
while (endIndex + 1 < lines.length) {
137+
const next = lines[endIndex + 1];
138+
if (next.trim() && !/^[ \t]/.test(next)) break;
139+
endIndex += 1;
140+
if (!next.trim()) {
141+
block.push(next);
142+
continue;
143+
}
144+
const indent = agentSkillLineIndent(next);
145+
if (indent == null) return null;
146+
if (!contentIndent) contentIndent = indent;
147+
if (!contentIndent || indent < contentIndent) return null;
148+
block.push(next);
149+
}
150+
151+
return {
152+
value: cleanText(block.map((line) => line.slice(contentIndent)).join('\n')),
153+
endIndex,
154+
};
155+
}
156+
157+
function parseAgentSkillMetadataMap(lines, startIndex) {
158+
let endIndex = startIndex;
159+
let mappingIndent = 0;
160+
let foundEntry = false;
161+
const seenKeys = new Set();
162+
163+
while (endIndex + 1 < lines.length) {
164+
const next = lines[endIndex + 1];
165+
if (next.trim() && !/^[ \t]/.test(next)) break;
166+
endIndex += 1;
167+
if (!next.trim() || /^\s*#/.test(next)) continue;
168+
const indent = agentSkillLineIndent(next);
169+
if (indent == null) return null;
170+
if (!mappingIndent) mappingIndent = indent;
171+
if (!mappingIndent || indent !== mappingIndent) return null;
172+
const entry = next.slice(mappingIndent).match(/^([^:#][^:]*):(?:[ \t]*(.*))?$/);
173+
const entryKey = cleanSingleLine(entry?.[1]);
174+
if (!entry || !entryKey || seenKeys.has(entryKey) || parseAgentSkillScalar(entry[2]) == null) return null;
175+
seenKeys.add(entryKey);
176+
foundEntry = true;
177+
}
178+
179+
return { value: foundEntry ? true : '', endIndex };
180+
}
181+
182+
function parseAgentSkillFrontmatter(content) {
183+
const text = String(content || '').replace(/\r\n?/g, '\n').trimStart();
184+
const match = text.match(/^---\n([\s\S]{0,8192}?)\n---(?:\n|$)/);
185+
if (!match) return null;
186+
187+
const lines = match[1].split('\n');
188+
const values = {};
189+
const seen = new Set();
190+
const allowedFields = new Set([
191+
'name',
192+
'description',
193+
'license',
194+
'compatibility',
195+
'metadata',
196+
'allowed-tools',
197+
]);
198+
for (let index = 0; index < lines.length; index += 1) {
199+
const line = lines[index];
200+
if (!line.trim() || /^\s*#/.test(line)) continue;
201+
if (/^[ \t]/.test(line)) return null;
202+
const field = line.match(/^([A-Za-z][A-Za-z0-9_-]*):(?:[ \t]*(.*))?$/);
203+
if (!field) return null;
204+
const key = field[1];
205+
if (!allowedFields.has(key)) return null;
206+
if (seen.has(key)) return null;
207+
seen.add(key);
208+
209+
const rawValue = field[2] || '';
210+
const scalarIndicator = rawValue.trim();
211+
if (key === 'metadata' && !scalarIndicator) {
212+
const parsed = parseAgentSkillMetadataMap(lines, index);
213+
if (!parsed) return null;
214+
values[key] = parsed.value;
215+
index = parsed.endIndex;
216+
} else if (/^[>|]/.test(scalarIndicator)) {
217+
const parsed = parseAgentSkillBlock(lines, index, scalarIndicator);
218+
if (!parsed) return null;
219+
values[key] = parsed.value;
220+
index = parsed.endIndex;
221+
} else {
222+
let scalar = parseAgentSkillScalar(rawValue);
223+
if (scalar == null && !scalarIndicator && key !== 'name' && key !== 'description') {
224+
scalar = '';
225+
} else if (scalar == null) {
226+
return null;
227+
}
228+
const continuation = [];
229+
while (index + 1 < lines.length) {
230+
const next = lines[index + 1];
231+
if (next.trim() && !/^[ \t]/.test(next)) break;
232+
index += 1;
233+
if (!next.trim() || /^\s*#/.test(next)) continue;
234+
const indent = agentSkillLineIndent(next);
235+
if (indent == null || !indent) return null;
236+
const part = parseAgentSkillScalar(next.slice(indent));
237+
if (part == null) return null;
238+
continuation.push(part);
239+
}
240+
values[key] = cleanSingleLine([scalar || '', ...continuation].join(' '));
241+
}
242+
}
243+
244+
const name = cleanSingleLine(values.name).normalize('NFKC');
245+
const description = cleanSingleLine(values.description);
246+
if (
247+
!/^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$/u.test(name)
248+
|| name !== name.toLowerCase()
249+
|| [...name].length > 64
250+
|| !description
251+
|| [...description].length > 1024
252+
|| (seen.has('compatibility') && [...cleanSingleLine(values.compatibility)].length > 500)
253+
) return null;
254+
return {
255+
name,
256+
description,
257+
body: cleanText(text.slice(match[0].length)),
258+
};
259+
}
260+
100261
function toolBlockRegex() {
101262
return /```(?:webbrain-tools|wb-tools)\s*\n([\s\S]*?)```/gi;
102263
}
@@ -106,7 +267,8 @@ function skillMetadataBlockRegex() {
106267
}
107268

108269
export function stripSkillToolBlocks(content) {
109-
return cleanText(content)
270+
const agentSkill = parseAgentSkillFrontmatter(content);
271+
return cleanText(agentSkill ? agentSkill.body : content)
110272
.replace(toolBlockRegex(), '')
111273
.replace(skillMetadataBlockRegex(), '')
112274
.trim();
@@ -464,16 +626,25 @@ function normalizeSkills(value, { maxSkills = MAX_CUSTOM_SKILLS } = {}) {
464626
const sourceUrl = sourceType === 'url' || sourceType === 'built-in'
465627
? cleanSingleLine(item.sourceUrl || item.path).slice(0, 2048)
466628
: '';
467-
const name = cleanSingleLine(item.name).slice(0, 80) || inferName(content, skills.length);
468-
const metadata = parseSkillMetadataBlock(content);
629+
const agentSkill = parseAgentSkillFrontmatter(content);
630+
const name = cleanSingleLine(item.name).slice(0, 80)
631+
|| agentSkill?.name
632+
|| inferName(content, skills.length);
633+
// Agent Skills frontmatter is instruction-only metadata. WebBrain routing
634+
// manifests and network tools are trusted only when they occur in the
635+
// Markdown body after a valid frontmatter boundary.
636+
const manifestContent = agentSkill ? agentSkill.body : content;
637+
const metadata = parseSkillMetadataBlock(manifestContent);
469638
const trustedChromeWebStoreSkill = id === 'chrome-web-store-release'
470639
&& sourceType === 'built-in'
471640
&& sourceUrl === 'skills/chrome-web-store-release.md';
472641
// Never re-normalize privileged runtime tools as generic HTTP tools when
473642
// an already-normalized skill record passes through this boundary again.
474643
const toolRecords = trustedChromeWebStoreSkill
475644
? []
476-
: Array.isArray(item.tools) ? item.tools : parseSkillToolBlocks(content);
645+
: agentSkill
646+
? parseSkillToolBlocks(manifestContent)
647+
: Array.isArray(item.tools) ? item.tools : parseSkillToolBlocks(content);
477648
const normalizedTools = normalizeSkillTools(toolRecords, id);
478649
const privilegedTools = trustedBuiltInSkillTools(id, sourceType, sourceUrl);
479650
skills.push({
@@ -482,7 +653,9 @@ function normalizeSkills(value, { maxSkills = MAX_CUSTOM_SKILLS } = {}) {
482653
sourceType,
483654
sourceUrl,
484655
content,
485-
summary: metadata?.summary || inferSkillSummary(content, name),
656+
summary: metadata?.summary
657+
|| agentSkill?.description.slice(0, MAX_CUSTOM_SKILL_SUMMARY_CHARS)
658+
|| inferSkillSummary(content, name),
486659
modes: metadata?.modes || ['act'],
487660
intents: metadata?.intents || [],
488661
tools: [...normalizedTools, ...privilegedTools],

0 commit comments

Comments
 (0)