Fix: close SKILL.md frontmatter on an own-line ---, not on any --- substring - #367
Open
AmaadMartin wants to merge 1 commit into
Open
Fix: close SKILL.md frontmatter on an own-line ---, not on any --- substring#367AmaadMartin wants to merge 1 commit into
AmaadMartin wants to merge 1 commit into
Conversation
…bstring
parseFrontmatterYaml located the frontmatter block with content.split('---'),
which has no notion of lines: the first '---' anywhere after the opening
delimiter closed the block, including one inside a frontmatter value. A
SKILL.md with description: "a --- b" was rejected with a YAML syntax error,
and a file whose only '---' was inside a value parsed "successfully" into a
silently truncated mapping.
Match the block with a line-anchored pattern instead, so only a '---' that
occupies a line of its own closes the frontmatter. Both existing error
strings and the body handling (including the tables-in-body guard) are
unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Link to an existing issue (if applicable):
Closes: #issue_number
Related: #issue_number
Or, if no issue exists, describe the change:
Problem:
parseFrontmatterYamlincore/src/skills/loader.tslocated the SKILL.md frontmatter block withcontent.split('---').String.prototype.splithas no notion of lines, so the first---anywhere after the opening delimiter — including one inside a frontmatter value — closed the block. Two user-visible failures:Bogus YAML error. A well-formed SKILL.md with
description: "a --- b"truncates todescription: "aandjs-yamlreportsunexpected end of the stream within a double quoted scalar (4:1), wrapped asInvalid YAML in frontmatter: …. The skill cannot be loaded, listed, or validated.Silent data corruption. When the truncation happens to leave valid YAML, the file loads with silently rewritten fields.
---\nname: t\ndescription: a --- b\nBodyhas no own-line closing delimiter at all, yet parsed "successfully" to{name: 't', description: 'a'}.Reachable from every entry point that reads a SKILL.md:
loadSkillFromDir,loadSkillFile,validateSkillDir,loadAllSkillsInDir(which logsSkipping invalid skill in '<dir>'and drops the skill) andloadSkillFromZipBuffer. The body side of this bug was already fixed in google#500 (markdown tables survive); the frontmatter side was not.Solution: Match the leading block with one line-anchored pattern instead of splitting on a substring, so only a
---that occupies a line of its own closes the frontmatter — the contract the docs already describe and that Jekyll, gray-matter andpython-frontmatterall implement:Four statements replace the eleven-line split. Design notes:
[ \t]*after each---preserves today's tolerance of a delimiter line with trailing spaces.([\s\S]*?\r?\n)?optional as a whole (rather than the newline optional inside it) is load-bearing: it lets---\n---\nmatch while still forcing every candidate closing---to sit immediately after a line break. The tempting([\s\S]*?)(?:\r?\n)?---reintroduces the bug for a value that ends a line with---, which is why there is a dedicated test for it.(?:\r?\n|$)lets the file end right after the closing delimiter (existinghandles empty bodytest);\r?\non both delimiters handles CRLF files.gflag, so the module-level regex carries nolastIndexstate across calls. The lazy[\s\S]*?is followed by a required line break plus a literal, so scanning stays linear — no nested quantifiers, no ReDoS surface.undefinedonly for the empty-frontmatter case, hence?? '';yaml.load('')returnsundefined, which the existing non-mapping check already rejects, so that degenerate input keeps producing the same error as before.No new dependency (
js-yamlremains the only YAML parser), no signature change, no new public export, and both existing error strings are preserved verbatim.Behaviour deltas (no previously-valid file becomes invalid):
---inside a quoted/plain frontmatter valueInvalid YAML in frontmatter: …, or silent truncation---, but a---inside a valueSKILL.md frontmatter not properly closed with ------inside a frontmatter block scalar----(4+ dashes) as the opening lineInvalid YAML in frontmatter: …SKILL.md frontmatter not properly closed with ------not followed by a line break (e.g.---name: x)SKILL.md frontmatter not properly closed with ---The last two rows change only which error is raised for input that was already an error.
Deliberately out of scope (each has its own queued task): the
Invalid YAML in frontmatter:wrapping of the non-mapping error, the redundant secondFrontmatterSchema.parseinloadSkillFile, and the identicalcontent.split("---", 2)defect in adk-python's_parse_skill_md_content. This change touches only the delimiter-detection lines.Collision check. Listed all 272 open PRs on the fork; 22 titles mention skills or frontmatter, and 9 of those touch
core/src/skills/loader.ts(#282, #283, #284, #263, #262, #242, #239, #310, #361). Grepping every one of those diffs forsplit('---'),parseFrontmatterYamlandproperly closedreturns nothing — none of them lands this change. The nearest neighbour is #282 (narrowing the frontmatter error messages), which rewrites the post-parse error handling of the same function; it was written against an oldermainwhere that logic still lived inparseSkillMdContent, and it does not touch the delimiter lines. Branched frommain(currently fcfd043) rather than stacking, because the two changes are line-disjoint and the delimiter fix should not have to wait on an error-message refactor.Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
Ten new
it()blocks incore/test/skills/loader_test.ts. No existing test was edited, renamed, weakened, skipped or deleted — the pre-existing suite (in particularhandles tables in body, the google#500 body guard, andthrows error if frontmatter is not properly closed) is the regression signal and still passes unchanged.describe('parseSkillMdContent'):keeps a quoted frontmatter value containing ---(the reported repro),keeps a frontmatter value with --- mid-line,keeps a frontmatter value whose line ends with ---,parses frontmatter with CRLF line endings,accepts a closing delimiter with trailing whitespace,throws when the only --- after the frontmatter is inside a value,preserves a --- horizontal rule in the body,still rejects a frontmatter block with no content. Plusloads a skill whose description contains ---(loadSkillFromDir, real files underfs.mkdtemp) andloads a zipped skill whose description contains ---(loadSkillFromZipBuffer, the second independent caller).Coverage of the new code, measured with
--coverage.include='core/src/skills/loader.ts': 100% of the new lines and branches (both arms of!match, both arms ofmatch[1] ?? '', the\r?\nand[ \t]*alternatives, and the$-terminated close via the pre-existinghandles empty body). No new uncovered statement or branch appears anywhere in the file.Proof the tests can fail. Coverage is not proof, so every new test was run against mutated source and confirmed to fail:
Restored the pre-fix
content.split('---')implementation verbatim — 7 of 10 fail:The remaining three pin preserved behaviour, so each was killed with a mutation of the exact construct it covers:
[ \t]*from the closing delimiter →× accepts a closing delimiter with trailing whitespace → SKILL.md frontmatter not properly closed with ---[\s\S]*instead of[\s\S]*?) →× preserves a --- horizontal rule in the body → Invalid YAML in frontmatter: expected a single document in the stream, but found morematch[1] ?? '{}'→× still rejects a frontmatter block with no content → expected […] to throw error including 'Invalid YAML in frontmatter: SKILL.md…' but got 'Invalid YAML in frontmatter: [\n {\n…'That last mutation initially survived a looser
toThrow('Invalid YAML in frontmatter:')assertion, because the schema-validation failure is wrapped with the same prefix. The assertion was therefore written against the full message,Invalid YAML in frontmatter: SKILL.md frontmatter must be a YAML mapping— matching the exact-message style already used elsewhere in this file.Integration:
npx vitest run --project integration tests/integration/skills→ 3 passed, 1 skipped, and one file (skills/script_js/agent_test.ts) fails in this sandbox because itsbeforeAllrunsnpm installwith no network. That failure reproduces identically on unmodifiedmain, so it is pre-existing and unrelated.npm run lintpasses.npm run format:checkpasses on both changed files.npm run ts:checkreports no error in either changed file; it exits non-zero for pre-existing errors elsewhere in the test tree that reproduce identically on unmodifiedmain.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
Against the built package, importing through the public entry point:
npm ci && npm run buildCreate a scratch skill directory and write a SKILL.md whose description is
"a --- b"and whose body contains both a markdown table and a---horizontal rule:In a scratch script,
import {validateSkillDir, loadAllSkillsInDir} from '@google/adk'and assertvalidateSkillDir(<skillDir>)returns[],loadAllSkillsInDir(<baseDir>)returns the skill,frontmatter.description === 'a --- b', andinstructionsis byte-identical to the body above.Result with this change: all assertions hold (
validateSkillDir=[] description="a --- b" body byte-identical). Re-running the same script against the pre-fix build fails at the first assertion with:Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.