Skip to content

feat(skills): support Agent Skills frontmatter - #517

Open
alectimison-maker wants to merge 5 commits into
webbrain-one:mainfrom
alectimison-maker:feat/agent-skills-frontmatter
Open

feat(skills): support Agent Skills frontmatter#517
alectimison-maker wants to merge 5 commits into
webbrain-one:mainfrom
alectimison-maker:feat/agent-skills-frontmatter

Conversation

@alectimison-maker

@alectimison-maker alectimison-maker commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • recognize valid Agent Skills SKILL.md frontmatter when importing custom skills
  • use name and description in the existing routing catalog and remove frontmatter before loading Markdown instructions
  • keep WebBrain routing/tool manifests confined to the Markdown body, never Agent Skills frontmatter
  • document the instruction-only trust boundary in English, French, and Chinese

Motivation

Agent Skills files otherwise import as plain text, so required frontmatter can become the inferred display name and is repeated in the loaded prompt. Supporting the standard routing fields lets existing SKILL.md instructions work without manual conversion.

The parser also sits on a security boundary: fenced webbrain-skill or webbrain-tools text inside a YAML block scalar must remain ordinary frontmatter data and must not grant Ask eligibility, routing intents, or network tools.

Design

The dependency-free parser follows the current agentskills/skills-ref behavior needed by WebBrain:

  • only the six specification fields are accepted: name, description, license, compatibility, metadata, and allowed-tools
  • StrictYAML-style plain scalar values such as true, 123, and dates remain strings, while malformed mapping separators and flow collections reject the parse
  • wrapped plain scalars and block scalars, including explicit indentation indicators, are supported
  • block indentation is established by the first content line; under-indented and tab-indented content is rejected
  • names are NFKC-normalized and validated as lowercase Unicode alphanumeric segments separated by single hyphens
  • invalid frontmatter is preserved as ordinary text rather than partially stripped

After valid frontmatter is parsed, webbrain-skill and webbrain-tools discovery scans only agentSkill.body. Re-normalization also rebuilds tools from that body instead of preserving a previously derived item.tools array. This closes the frontmatter/body trust-boundary bypass while retaining legitimate WebBrain manifests placed in the Markdown body.

Existing precedence remains:

  • a name entered in Settings overrides the Agent Skills name
  • a body-level webbrain-skill block overrides the standard routing summary and may explicitly enable supported modes/intents
  • Agent Skills allowed-tools never grants WebBrain tools, permissions, or Ask eligibility

This avoids adding a YAML/runtime dependency to both browser builds and introduces no network access, permissions, or code execution.

Testing

  • node test/run.js — 1358 passed; the one failure is the existing upstream release-history mismatch (package.json 26.0.1 vs. latest CHANGELOG.md entry 26.0.0)
  • node test/security/injection-corpus.mjs — 60/60 checks passed
  • node --check src/chrome/src/agent/skills.js — passed
  • node --check src/firefox/src/agent/skills.js — passed
  • Chrome/Firefox parser parity comparison — passed
  • git diff --check — passed

Regression coverage includes hidden frontmatter manifests/tools, re-normalization, legitimate body-level tools, StrictYAML string cases, malformed plain scalars, allowed/unknown/duplicate fields, explicit and invalid indentation, empty optional fields, international names, uppercase rejection, and NFKC normalization.

Compatibility and risks

Existing plain-text and body-level webbrain-skill / webbrain-tools imports retain their behavior. The parser intentionally implements the bounded Agent Skills frontmatter subset WebBrain consumes instead of adding a general YAML dependency. Residual risk is future divergence if the reference grammar changes.

Scope

This does not import Agent Skills directories or fetch scripts/, references/, or assets/. It does not execute bundled code or interpret allowed-tools; those capabilities require a separate security and packaging design.

@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown

@alectimison-maker is attempting to deploy a commit to the esokullu's projects Team on Vercel.

A member of the Team first needs to authorize it.

@esokullu

Copy link
Copy Markdown
Collaborator

Review: PR #517 — feat(skills): support Agent Skills frontmatter

Overview

Teaches the custom-skill importer to recognize the two required fields (name, description) of an Agent Skills SKILL.md YAML frontmatter, via a small hand-rolled parser (parseAgentSkillFrontmatter + parseAgentSkillScalar) added identically to src/chrome/src/agent/skills.js and src/firefox/src/agent/skills.js. Valid frontmatter feeds the routing catalog (name/summary) and is stripped before the Markdown body is loaded; invalid frontmatter is left untouched as plain text. Docs updated in en/fr/zh-CN; three new multi-case tests cover both builds.

What's done well

  • Precedence is right and tested: Settings name → Agent Skills name → inferred; webbrain-skill summary/modes/intents always win. Tests assert both directions.
  • Security posture is sound: allowed-tools is deliberately ignored (asserted in tests — skill.tools stays empty, no Ask eligibility, no invented intents). No new dependency, no network access. The regexes are anchored, bounded ({0,8192}? lazy match), and free of catastrophic-backtracking patterns; input is already capped by MAX_CUSTOM_SKILL_CHARS.
  • Conservative failure mode: any parse ambiguity (duplicate keys, malformed top-level line, non-string/typed scalars like name: true, oversized fields) returns null and the file is treated as ordinary text rather than being partially mangled. The "invalid frontmatter was destructively stripped" assertion is a nice guard.
  • YAML plain-scalar comment stripping (\s+#.*$) actually matches real YAML semantics — C# survives, # comment is dropped. Good attention to detail.
  • Consistency between the two strip paths: normalizeSkills and stripSkillToolBlocks call the same parser, so the catalog and the loaded prompt can't disagree about whether frontmatter was valid.

Issues

  1. Multi-line plain scalars are silently truncated, and the tail is lost entirely (moderate).
    In the frontmatter line loop, indented continuation lines are skipped (/^\s/.test(line) → continue). A hand-wrapped description in plain style —

description: Extract PDF text and fill forms.
Use when working with PDF documents.

— is valid YAML (folds to one string) and passes validation here, but the parser accepts it with description = "Extract PDF text and fill forms." only. Worse, the continuation line lives inside the frontmatter block, so it's also stripped from the loaded body: the text is lost from both the catalog and the instructions. This contradicts the PR's stated principle that unsupported forms "remain unmodified text." Suggest either (a) folding continuation lines into the value, or (b) detecting an indented line immediately after name:/description: plain scalars and returning null (reject → keep as text), which fits the conservative design better.

  1. Block-scalar indicators with explicit indentation aren't recognized (minor).
    /^[>|][+-]?$/ misses forms like >2 or |2-. description: >2 falls through to parseAgentSkillScalar, which returns the literal string ">2" — a syntactically "valid" but garbage description, and the indented block below is silently skipped. Rejecting (return null) on /^[>|]/ values that don't match the supported indicator form would be safer.

  2. Leading-whitespace sensitivity in stripSkillToolBlocks (low/latent).
    parseAgentSkillFrontmatter strips BOM and normalizes CRLF but does not trim, while stripSkillToolBlocks calls it before cleanText. Today all callers pass content already run through cleanText (normalizeSkills stores cleaned content), so behavior is consistent — but a future direct caller passing raw content with a leading blank line would get the frontmatter kept in the prompt while a normalized copy strips it. A .trimStart()-equivalent inside the parser (or a comment stating the precondition) would close this.

Nits / observations

  • Redundant parse work: normalizeSkills parses the frontmatter once per skill, and every prompt build re-parses via stripSkillToolBlocks (skills.js:637). Bounded and cheap, but the frontmatter's validity could be derived once at normalize time if this ever shows up in profiles. Not blocking.
  • Chrome/Firefox duplication is the established convention in this repo and the two copies are byte-identical here — fine, just make sure future fixes (e.g. issue 1) land in both.
  • Pre-existing, not introduced here: for invalid frontmatter with no webbrain-skill block, inferSkillSummary still builds the summary from the ---/name:/description: lines themselves (they're neither headings nor fences, so they form the "first paragraph"). The PR fixes this for valid frontmatter; the invalid path keeps the old junk-summary behavior. Worth a follow-up.
  • The frontmatter size cap (8192) and the MAX_CUSTOM_SKILL_CHARS truncation interact benignly (truncated frontmatter just fails to match), no action needed.

Test coverage

Strong: valid folded scalar, precedence, invalid-name fallback, duplicates, implicit boolean, non-string flow sequence, oversized description, leak assertions into the built prompt, all run against both builds. Gaps worth adding, especially given issue 1:

  • a plain multi-line (wrapped) description — currently would document the truncation bug;
  • CRLF and BOM-prefixed input (the parser handles both, but nothing pins it);
  • frontmatter exceeding 8192 chars falling back to plain text.

Verdict

Well-scoped, security-conscious change that matches the project's conventions and trust model. I'd ask for issue 1 (multi-line plain scalar data loss) to be fixed or explicitly rejected-as-invalid before merge; issues 2–3 and the extra tests are nice-to-haves. @alectimison-maker

@alectimison-maker

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in 18c18ede after merging the latest main:

  • wrapped plain scalars after name/description now reject the entire conservative parse, so the continuation and body remain unchanged instead of being silently truncated;
  • unsupported explicit indentation indicators such as >2- are rejected rather than becoming a literal summary;
  • direct stripping now normalizes BOM, CRLF, and leading whitespace consistently with imported skill normalization;
  • regression coverage pins both browser builds, including the 8 KiB frontmatter fallback.

Validation: node test/run.js 1353/1354 (only current-main package.json 26.0.1 vs. CHANGELOG.md 26.0.0), security 60/60, and syntax/diff checks pass.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Agent Skills frontmatter support to custom skill imports.

Changes:

  • Parses name and description metadata in both browser builds.
  • Strips valid frontmatter before loading instructions.
  • Adds parity tests and multilingual documentation.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/chrome/src/agent/skills.js Implements Chrome parsing and routing integration.
src/firefox/src/agent/skills.js Mirrors the Chrome implementation.
test/run.js Tests parsing, precedence, rejection, and preservation.
docs/skills.md Documents compatibility and limitations.
docs/architecture.md Describes routing and prompt behavior.
docs/fr/skills.md Adds French user documentation.
docs/fr/architecture.md Adds French architecture documentation.
docs/zh-CN/skills.md Adds Chinese user documentation.
docs/zh-CN/architecture.md Adds Chinese architecture documentation.
Comments suppressed due to low confidence (6)

src/chrome/src/agent/skills.js:148

  • This derives indentation from the minimum across all block lines, but YAML derives it from the first non-empty line. Consequently an invalid block such as a 4-space first line followed by a 2-space line (or tab indentation) is accepted and its frontmatter is destructively stripped. Reject under-indented/tab-indented content so unsupported YAML remains ordinary skill text as intended.
      const nonEmpty = block.filter((item) => item.trim());
      const indent = nonEmpty.length
        ? Math.min(...nonEmpty.map((item) => item.match(/^\s*/)[0].length))
        : 0;
      values[key] = cleanText(block.map((item) => item.slice(indent)).join('\n'));

src/firefox/src/agent/skills.js:148

  • This derives indentation from the minimum across all block lines, but YAML derives it from the first non-empty line. Consequently an invalid block such as a 4-space first line followed by a 2-space line (or tab indentation) is accepted and its frontmatter is destructively stripped. Reject under-indented/tab-indented content so unsupported YAML remains ordinary skill text as intended.
      const nonEmpty = block.filter((item) => item.trim());
      const indent = nonEmpty.length
        ? Math.min(...nonEmpty.map((item) => item.match(/^\s*/)[0].length))
        : 0;
      values[key] = cleanText(block.map((item) => item.slice(indent)).join('\n'));

src/chrome/src/agent/skills.js:166

  • The current Agent Skills validator permits lowercase international names (for example 技能 and мой-навык) and NFKC-normalizes them, but this ASCII-only regex rejects those valid skills and leaves their frontmatter in the prompt. Validate Unicode letters/numbers with lowercase and NFKC checks; count code points rather than UTF-16 units so the 64/1024-character limits also match the specification.
  const name = cleanSingleLine(values.name);
  const description = cleanSingleLine(values.description);
  if (
    !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)
    || name.length > 64
    || !description
    || description.length > 1024

src/firefox/src/agent/skills.js:166

  • The current Agent Skills validator permits lowercase international names (for example 技能 and мой-навык) and NFKC-normalizes them, but this ASCII-only regex rejects those valid skills and leaves their frontmatter in the prompt. Validate Unicode letters/numbers with lowercase and NFKC checks; count code points rather than UTF-16 units so the 64/1024-character limits also match the specification.
  const name = cleanSingleLine(values.name);
  const description = cleanSingleLine(values.description);
  if (
    !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)
    || name.length > 64
    || !description
    || description.length > 1024

src/chrome/src/agent/skills.js:127

  • Only known optional Agent Skills fields should be ignored. This branch currently accepts every unknown top-level key, but the official validator rejects unexpected fields; such a file is therefore treated as valid and stripped even though it is not a valid Agent Skill. Reject keys outside name, description, license, allowed-tools, metadata, and compatibility.
    const key = field[1];
    if (key !== 'name' && key !== 'description') continue;

src/firefox/src/agent/skills.js:127

  • Only known optional Agent Skills fields should be ignored. This branch currently accepts every unknown top-level key, but the official validator rejects unexpected fields; such a file is therefore treated as valid and stripped even though it is not a valid Agent Skill. Reject keys outside name, description, license, allowed-tools, metadata, and compatibility.
    const key = field[1];
    if (key !== 'name' && key !== 'description') continue;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

? cleanSingleLine(item.sourceUrl || item.path).slice(0, 2048)
: '';
const name = cleanSingleLine(item.name).slice(0, 80) || inferName(content, skills.length);
const agentSkill = parseAgentSkillFrontmatter(content);
? cleanSingleLine(item.sourceUrl || item.path).slice(0, 2048)
: '';
const name = cleanSingleLine(item.name).slice(0, 80) || inferName(content, skills.length);
const agentSkill = parseAgentSkillFrontmatter(content);
Comment thread src/chrome/src/agent/skills.js Outdated
return null;
}
}
return raw.replace(/\s+#.*$/, '').trim() || null;
Comment thread src/firefox/src/agent/skills.js Outdated
return null;
}
}
return raw.replace(/\s+#.*$/, '').trim() || null;
@esokullu

Copy link
Copy Markdown
Collaborator

The parser crosses the frontmatter/body trust boundary, allowing hidden WebBrain metadata and tools. It also diverges from the reference Agent Skills grammar and validation rules, rejecting valid skills while destructively accepting malformed ones.

Full review comments:

  • [P1] Restrict WebBrain manifests to the Markdown bodysrc/chrome/src/agent/skills.js:543

    When valid frontmatter contains an indented webbrain-skill or webbrain-tools fence inside a block scalar, this parse succeeds but metadata and tool discovery below still scan the original content. Such frontmatter can grant Ask eligibility or register an HTTPS tool despite the instruction-only guarantee; parse manifests only from agentSkill.body and avoid preserving frontmatter-derived item.tools during re-normalization. The Firefox copy has the same issue.

  • [P2] Match the reference scalar grammarsrc/chrome/src/agent/skills.js:90

    For imported skills with unquoted values, this parser rejects valid StrictYAML strings such as name: true, name: 123, or description: 2026-07-27, while accepting invalid plain scalars such as description: foo: bar. This causes valid Agent Skills to fall back and malformed frontmatter to be destructively stripped; implement the reference scalar grammar in both browser copies.

  • [P2] Reject unsupported top-level frontmatter keyssrc/chrome/src/agent/skills.js:126-127

    When otherwise valid frontmatter includes an unknown top-level key, skills-ref validate rejects the document, but this unconditional skip accepts it and later removes the entire block. That violates the stated preservation behavior for invalid frontmatter; only the six Agent Skills fields should be accepted, with the mirrored Firefox parser updated as well.

  • [P2] Validate block indentation from the first content linesrc/chrome/src/agent/skills.js:144-148

    For a block scalar whose first non-empty line has four spaces and a later line has only two, YAML treats the document as malformed because the first line establishes the indentation. Taking the minimum accepts this input, uses it as routing metadata, and strips the invalid frontmatter; reject under-indented or tab-indented lines instead in both browser copies.

  • [P2] Accept valid international skill namessrc/chrome/src/agent/skills.js:160-164

    When an Agent Skill uses a lowercase international name such as 技能 or мой-навык, the official validator accepts it after NFKC normalization, but this ASCII-only expression rejects it. The skill then loses its routing name and description and retains raw frontmatter in the loaded prompt; apply the reference Unicode lowercase/alphanumeric validation in both browser implementations.

@alectimison-maker

Copy link
Copy Markdown
Contributor Author

Addressed the five review findings in 5deff4fd after merging the latest main:

  • P1 trust boundary: webbrain-skill and webbrain-tools discovery now scans only the parsed Markdown body. Re-normalization rebuilds tools from that body, so block-scalar fences and stale item.tools cannot grant routing or network capabilities.
  • Reference scalar grammar: plain true, numeric, and date-like values remain strings; wrapped plain scalars are folded; malformed mapping separators, flow values, and unterminated quotes reject without stripping the source.
  • Top-level keys: only the six Agent Skills fields are accepted, with duplicate/unknown fields rejected.
  • Block indentation: implicit indentation comes from the first content line; explicit indicators are supported; under-indented and tab-indented content is rejected.
  • International names: names are NFKC-normalized and validated with lowercase Unicode letter/number segments and single hyphens.

I also added coverage proving legitimate webbrain-tools fences in the Markdown body still work, and updated EN/FR/ZH trust-boundary documentation.

Validation: node test/run.js 1358/1359 (only the existing package.json 26.0.1 vs. CHANGELOG.md 26.0.0 baseline failure), security corpus 60/60, Chrome/Firefox syntax and parser-parity checks, and git diff --check all pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants