Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

Loading is idempotent and multiple relevant skills can be active in one run.
Expand Down
2 changes: 1 addition & 1 deletion docs/fr/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ while (steps < maxSteps) {

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.

`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.
`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.

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.

Expand Down
17 changes: 17 additions & 0 deletions docs/fr/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ correspondance littérale de mots-clés. Les compétences sans métadonnées
déduisent leur résumé du premier paragraphe de prose, n'ont aucune intention
déduite, et utilisent Act/Dev par défaut.

WebBrain reconnaît également les métadonnées YAML obligatoires `name` et
`description` d'un fichier
[Agent Skills `SKILL.md`](https://agentskills.io/specification) importé. Le nom
et la description alimentent le catalogue de routage, et les métadonnées sont
retirées avant le chargement du corps Markdown. Un nom saisi dans Paramètres et
un bloc `webbrain-skill` restent prioritaires.

Cette compatibilité concerne uniquement les instructions. WebBrain importe un
seul document texte ; il ne récupère pas les répertoires `scripts/`,
`references/` ou `assets/`, n'exécute pas le code de la compétence et ne traite
pas le champ Agent Skills `allowed-tools` comme une permission ou un manifeste
d'outils WebBrain. Utilisez `webbrain-tools` pour les outils HTTP WebBrain.
WebBrain ne reconnaît les blocs `webbrain-skill` et `webbrain-tools` que dans
le corps Markdown après des métadonnées valides ; du texte ressemblant à un
bloc dans les métadonnées ne peut ni autoriser le routage ni enregistrer
d'outil.

## Outils de compétence

Une compétence peut exposer des outils HTTP en lecture seule, ou des outils de
Expand Down
14 changes: 14 additions & 0 deletions docs/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ Intents are cross-language *meaning* hints for the LLM, not literal keyword
matching. Skills without metadata infer the first prose paragraph as their
summary, have no inferred intents, and default to Act/Dev.

WebBrain also recognizes the required `name` and `description` YAML frontmatter
from an imported [Agent Skills `SKILL.md`](https://agentskills.io/specification).
The name and description populate the routing catalog, and the frontmatter is
removed before the Markdown body is loaded. A name entered in Settings and a
`webbrain-skill` block still take precedence.

This is instruction-only compatibility. WebBrain imports one text document; it
does not fetch bundled `scripts/`, `references/`, or `assets/`, execute skill
code, or treat the Agent Skills `allowed-tools` field as a WebBrain permission
or tool manifest. Use `webbrain-tools` for WebBrain HTTP tools. WebBrain
recognizes `webbrain-skill` and `webbrain-tools` fences only in the Markdown
body after valid frontmatter; fence-like text inside frontmatter cannot grant
routing eligibility or register tools.

## Skill tools

A skill can expose read-only HTTP tools, or short-lived download-job tools, with
Expand Down
2 changes: 1 addition & 1 deletion docs/zh-CN/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ while (steps < maxSteps) {

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

技能(`skills.js`)会规范每个技能,并为规划器和保留工具 `load_skill` 生成同一个 `{id, name, summary, intents}` 路由目录。可选的 `webbrain-skill` 块最多声明 6 个唯一的小写意图标识符,每个不超过 40 个字符,并符合 `[a-z0-9][a-z0-9_-]*`。这些意图是与语言无关的语义路由提示,不是必须逐字匹配的关键词或子字符串;未声明意图的技能不会被自动推断标签。
技能(`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 读取。

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

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

WebBrain 也会识别导入的
[Agent Skills `SKILL.md`](https://agentskills.io/specification) 中必需的
`name` 和 `description` YAML frontmatter。名称和描述会写入路由目录,而在加载
Markdown 正文前会移除 frontmatter。在设置中输入的名称及 `webbrain-skill` 块仍有
更高优先级。

这仅属于指令兼容。WebBrain 只导入一个文本文档;它不会获取打包的 `scripts/`、
`references/` 或 `assets/`,不会执行技能代码,也不会把 Agent Skills 的
`allowed-tools` 字段当作 WebBrain 权限或工具清单。WebBrain HTTP 工具仍应使用
`webbrain-tools`。WebBrain 只识别有效 frontmatter 之后 Markdown 正文中的
`webbrain-skill` 和 `webbrain-tools` 围栏;frontmatter 内形似围栏的文本不能授予
路由资格或注册工具。

## 技能工具

技能可以通过围栏 `webbrain-tools` JSON 清单暴露只读 HTTP 工具,或短生命周期的
Expand Down
179 changes: 174 additions & 5 deletions src/chrome/src/agent/skills.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,163 @@ function inferName(content, index) {
return (firstLine || `Skill ${index + 1}`).slice(0, 80);
}

function parseAgentSkillScalar(value) {
const raw = String(value || '').trim();
if (!raw || /^[!&*\[{]/.test(raw)) return null;
const singleQuoted = raw.match(/^'((?:''|[^'])*)'(?:\s+#.*)?$/);
if (singleQuoted) return singleQuoted[1].replace(/''/g, "'");
const doubleQuoted = raw.match(/^("(?:\\.|[^"\\])*")(?:\s+#.*)?$/);
if (doubleQuoted) {
try {
const parsed = JSON.parse(doubleQuoted[1]);
return typeof parsed === 'string' ? parsed : null;
} catch {
return null;
}
}
if (/^['"]/.test(raw)) return null;
const plain = raw.replace(/\s+#.*$/, '').trim();
if (!plain || /:(?:[ \t]|$)/.test(plain)) return null;
return plain;
}

function agentSkillLineIndent(line) {
const leading = String(line || '').match(/^[ \t]*/)?.[0] || '';
return leading.includes('\t') ? null : leading.length;
}

function parseAgentSkillBlock(lines, startIndex, indicator) {
if (!/^[>|](?:(?:[1-9][+-]?)|(?:[+-][1-9]?))?$/.test(indicator)) return null;
const explicitIndent = Number(indicator.match(/[1-9]/)?.[0] || 0);
const block = [];
let endIndex = startIndex;
let contentIndent = explicitIndent || 0;

while (endIndex + 1 < lines.length) {
const next = lines[endIndex + 1];
if (next.trim() && !/^[ \t]/.test(next)) break;
endIndex += 1;
if (!next.trim()) {
block.push(next);
continue;
}
const indent = agentSkillLineIndent(next);
if (indent == null) return null;
if (!contentIndent) contentIndent = indent;
if (!contentIndent || indent < contentIndent) return null;
block.push(next);
}

return {
value: cleanText(block.map((line) => line.slice(contentIndent)).join('\n')),
endIndex,
};
}

function parseAgentSkillMetadataMap(lines, startIndex) {
let endIndex = startIndex;
let mappingIndent = 0;
let foundEntry = false;
const seenKeys = new Set();

while (endIndex + 1 < lines.length) {
const next = lines[endIndex + 1];
if (next.trim() && !/^[ \t]/.test(next)) break;
endIndex += 1;
if (!next.trim() || /^\s*#/.test(next)) continue;
const indent = agentSkillLineIndent(next);
if (indent == null) return null;
if (!mappingIndent) mappingIndent = indent;
if (!mappingIndent || indent !== mappingIndent) return null;
const entry = next.slice(mappingIndent).match(/^([^:#][^:]*):(?:[ \t]*(.*))?$/);
const entryKey = cleanSingleLine(entry?.[1]);
if (!entry || !entryKey || seenKeys.has(entryKey) || parseAgentSkillScalar(entry[2]) == null) return null;
seenKeys.add(entryKey);
foundEntry = true;
}

return { value: foundEntry ? true : '', endIndex };
}

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

const lines = match[1].split('\n');
const values = {};
const seen = new Set();
const allowedFields = new Set([
'name',
'description',
'license',
'compatibility',
'metadata',
'allowed-tools',
]);
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
if (!line.trim() || /^\s*#/.test(line)) continue;
if (/^[ \t]/.test(line)) return null;
const field = line.match(/^([A-Za-z][A-Za-z0-9_-]*):(?:[ \t]*(.*))?$/);
if (!field) return null;
const key = field[1];
if (!allowedFields.has(key)) return null;
if (seen.has(key)) return null;
seen.add(key);

const rawValue = field[2] || '';
const scalarIndicator = rawValue.trim();
if (key === 'metadata' && !scalarIndicator) {
const parsed = parseAgentSkillMetadataMap(lines, index);
if (!parsed) return null;
values[key] = parsed.value;
index = parsed.endIndex;
} else if (/^[>|]/.test(scalarIndicator)) {
const parsed = parseAgentSkillBlock(lines, index, scalarIndicator);
if (!parsed) return null;
values[key] = parsed.value;
index = parsed.endIndex;
} else {
let scalar = parseAgentSkillScalar(rawValue);
if (scalar == null && !scalarIndicator && key !== 'name' && key !== 'description') {
scalar = '';
} else if (scalar == null) {
return null;
}
const continuation = [];
while (index + 1 < lines.length) {
const next = lines[index + 1];
if (next.trim() && !/^[ \t]/.test(next)) break;
index += 1;
if (!next.trim() || /^\s*#/.test(next)) continue;
const indent = agentSkillLineIndent(next);
if (indent == null || !indent) return null;
const part = parseAgentSkillScalar(next.slice(indent));
if (part == null) return null;
continuation.push(part);
}
values[key] = cleanSingleLine([scalar || '', ...continuation].join(' '));
}
}

const name = cleanSingleLine(values.name).normalize('NFKC');
const description = cleanSingleLine(values.description);
if (
!/^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$/u.test(name)
|| name !== name.toLowerCase()
|| [...name].length > 64
|| !description
|| [...description].length > 1024
|| (seen.has('compatibility') && [...cleanSingleLine(values.compatibility)].length > 500)
) return null;
return {
name,
description,
body: cleanText(text.slice(match[0].length)),
};
}

function toolBlockRegex() {
return /```(?:webbrain-tools|wb-tools)\s*\n([\s\S]*?)```/gi;
}
Expand All @@ -106,7 +263,8 @@ function skillMetadataBlockRegex() {
}

export function stripSkillToolBlocks(content) {
return cleanText(content)
const agentSkill = parseAgentSkillFrontmatter(content);
return cleanText(agentSkill ? agentSkill.body : content)
.replace(toolBlockRegex(), '')
.replace(skillMetadataBlockRegex(), '')
.trim();
Expand Down Expand Up @@ -464,16 +622,25 @@ function normalizeSkills(value, { maxSkills = MAX_CUSTOM_SKILLS } = {}) {
const sourceUrl = sourceType === 'url' || sourceType === 'built-in'
? cleanSingleLine(item.sourceUrl || item.path).slice(0, 2048)
: '';
const name = cleanSingleLine(item.name).slice(0, 80) || inferName(content, skills.length);
const metadata = parseSkillMetadataBlock(content);
const agentSkill = parseAgentSkillFrontmatter(content);
const name = cleanSingleLine(item.name).slice(0, 80)
|| agentSkill?.name
|| inferName(content, skills.length);
// Agent Skills frontmatter is instruction-only metadata. WebBrain routing
// manifests and network tools are trusted only when they occur in the
// Markdown body after a valid frontmatter boundary.
const manifestContent = agentSkill ? agentSkill.body : content;
const metadata = parseSkillMetadataBlock(manifestContent);
const trustedChromeWebStoreSkill = id === 'chrome-web-store-release'
&& sourceType === 'built-in'
&& sourceUrl === 'skills/chrome-web-store-release.md';
// Never re-normalize privileged runtime tools as generic HTTP tools when
// an already-normalized skill record passes through this boundary again.
const toolRecords = trustedChromeWebStoreSkill
? []
: Array.isArray(item.tools) ? item.tools : parseSkillToolBlocks(content);
: agentSkill
? parseSkillToolBlocks(manifestContent)
: Array.isArray(item.tools) ? item.tools : parseSkillToolBlocks(content);
const normalizedTools = normalizeSkillTools(toolRecords, id);
const privilegedTools = trustedBuiltInSkillTools(id, sourceType, sourceUrl);
skills.push({
Expand All @@ -482,7 +649,9 @@ function normalizeSkills(value, { maxSkills = MAX_CUSTOM_SKILLS } = {}) {
sourceType,
sourceUrl,
content,
summary: metadata?.summary || inferSkillSummary(content, name),
summary: metadata?.summary
|| agentSkill?.description.slice(0, MAX_CUSTOM_SKILL_SUMMARY_CHARS)
|| inferSkillSummary(content, name),
modes: metadata?.modes || ['act'],
intents: metadata?.intents || [],
tools: [...normalizedTools, ...privilegedTools],
Expand Down
Loading
Loading