Repository navigation
Python: Add installable Agent Framework builder skill #9374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Eduard van Valkenburg (eavanvalkenburg)
merged 13 commits into
main
from
maf-consumer-skills
Oct 22, 2026
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
79d8ebd
Python: Add Agent Framework consumer skill
eavanvalkenburg a7c52b5
Address consumer skill review feedback
eavanvalkenburg e5a3e3d
Add Agent Skills validation workflow
eavanvalkenburg 6e7eb75
Expand skill extension and deployment guidance
eavanvalkenburg e42d6c5
Document custom ContextProvider guidance
eavanvalkenburg 3c44ac2
Add Agent Framework extensibility guide
eavanvalkenburg 0b43ab5
Link volatile guidance to authoritative docs
eavanvalkenburg bffcabf
Align consumer skill detail with documentation
eavanvalkenburg 91308f6
Clarify provider settings resolution
eavanvalkenburg b4b38be
Address Agent Skills review feedback
eavanvalkenburg 808de8e
Address remaining Agent Skills review feedback
eavanvalkenburg 18cd8df
Keep consumer skill guidance repository-scoped
eavanvalkenburg b1df0aa
Address latest Agent Skills review feedback
eavanvalkenburg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| #!/usr/bin/env python3 | ||
| # Copyright (c) Microsoft. All rights reserved. | ||
| """Validate local Markdown resources bundled with public Agent Skills.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| import stat | ||
| import sys | ||
| from pathlib import Path | ||
| from urllib.parse import urlsplit | ||
|
|
||
| INLINE_LINK_PATTERN = re.compile(r"!?\[[^\]\n]*\]\(\s*(<[^>\n]+>|[^)\s]+)") | ||
| REFERENCE_DEFINITION_PATTERN = re.compile(r"(?m)^[ \t]{0,3}\[(?!\^)[^\]\n]+\]:[ \t]*(?:<([^>\n]+)>|(\S+))") | ||
| REMOTE_SCHEMES = frozenset({"http", "https", "mailto"}) | ||
|
|
||
|
|
||
| def _markdown_targets(markdown: str) -> list[str]: | ||
| targets = [match.group(1).removeprefix("<").removesuffix(">") for match in INLINE_LINK_PATTERN.finditer(markdown)] | ||
| targets.extend( | ||
| next(group for group in match.groups() if group is not None) | ||
| for match in REFERENCE_DEFINITION_PATTERN.finditer(markdown) | ||
| ) | ||
| return targets | ||
|
|
||
|
|
||
| def validate_agent_skill_links(skills_directory: Path) -> list[str]: | ||
| """Return validation errors for local Markdown links in public Agent Skills.""" | ||
| errors: list[str] = [] | ||
| skill_roots = sorted(path.parent for path in skills_directory.rglob("SKILL.md")) | ||
|
|
||
| if not skill_roots: | ||
| return [f"No Agent Skills found under {skills_directory}."] | ||
|
|
||
| for skill_root in skill_roots: | ||
| resolved_root = skill_root.resolve(strict=True) | ||
| for markdown_path in skill_root.rglob("*.md"): | ||
| markdown = markdown_path.read_text(encoding="utf-8") | ||
| for target in _markdown_targets(markdown): | ||
| if target.startswith("#"): | ||
| continue | ||
|
|
||
| target_path = Path(target) | ||
| if target_path.is_absolute(): | ||
| errors.append(f"{markdown_path}: absolute local link is not allowed: {target}") | ||
| continue | ||
|
|
||
| parsed_target = urlsplit(target) | ||
| if parsed_target.scheme: | ||
| if parsed_target.scheme.lower() in REMOTE_SCHEMES: | ||
| continue | ||
| errors.append(f"{markdown_path}: unsupported URI scheme: {target}") | ||
| continue | ||
|
|
||
| relative_target = parsed_target.path | ||
| if not relative_target: | ||
| continue | ||
|
|
||
| target_path = Path(relative_target) | ||
| candidate = markdown_path.parent / target_path | ||
| try: | ||
| resolved_candidate = candidate.resolve(strict=True) | ||
| except FileNotFoundError: | ||
| errors.append(f"{markdown_path}: missing local resource: {target}") | ||
| continue | ||
|
|
||
| try: | ||
| resolved_candidate.relative_to(resolved_root) | ||
| except ValueError: | ||
| errors.append(f"{markdown_path}: local resource escapes skill root: {target}") | ||
| continue | ||
|
|
||
| if not stat.S_ISREG(candidate.lstat().st_mode): | ||
| errors.append(f"{markdown_path}: local resource is not a regular file: {target}") | ||
|
|
||
| return errors | ||
|
|
||
|
|
||
| def main() -> None: | ||
| skills_directory = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("skills") | ||
| errors = validate_agent_skill_links(skills_directory) | ||
| if errors: | ||
| raise SystemExit("\n".join(errors)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| # Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| # ruff:file-ignore[implicit-namespace-package, undocumented-public-class, undocumented-public-method] | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import sys | ||
| import tempfile | ||
| import unittest | ||
| from pathlib import Path | ||
|
|
||
| sys.path.insert(0, str(Path(__file__).parents[1] / "scripts")) | ||
|
|
||
| from validate_agent_skill_links import validate_agent_skill_links | ||
|
|
||
|
|
||
| class ValidateAgentSkillLinksTests(unittest.TestCase): | ||
| def setUp(self) -> None: | ||
| self.temp_dir = tempfile.TemporaryDirectory() | ||
| self.root = Path(self.temp_dir.name) | ||
| self.skills = self.root / "skills" | ||
| self.skill = self.skills / "example-skill" | ||
| self.skill.mkdir(parents=True) | ||
|
|
||
| def tearDown(self) -> None: | ||
| self.temp_dir.cleanup() | ||
|
|
||
| def write_skill(self, body: str) -> None: | ||
| (self.skill / "SKILL.md").write_text(body, encoding="utf-8") | ||
|
|
||
| def test_accepts_regular_file_inside_skill(self) -> None: | ||
| (self.skill / "references").mkdir() | ||
| (self.skill / "references" / "details.md").write_text("# Details\n", encoding="utf-8") | ||
| self.write_skill("[Details](references/details.md)\n") | ||
|
|
||
| self.assertEqual([], validate_agent_skill_links(self.skills)) | ||
|
|
||
| def test_accepts_reference_style_file_inside_skill(self) -> None: | ||
| (self.skill / "references").mkdir() | ||
| (self.skill / "references" / "details.md").write_text("# Details\n", encoding="utf-8") | ||
| self.write_skill("[Details][resource]\n\n[resource]: references/details.md\n") | ||
|
|
||
| self.assertEqual([], validate_agent_skill_links(self.skills)) | ||
|
|
||
| def test_accepts_remote_links_mailto_and_anchors(self) -> None: | ||
| self.write_skill( | ||
| "[HTTP](http://example.com)\n" | ||
| "[HTTPS][secure]\n" | ||
| "[Email](mailto:agent@example.com)\n" | ||
| "[Section](#section)\n" | ||
| "\n" | ||
| "[secure]: https://example.com\n" | ||
| ) | ||
|
|
||
| self.assertEqual([], validate_agent_skill_links(self.skills)) | ||
|
|
||
| def test_rejects_parent_traversal(self) -> None: | ||
| (self.skills / "outside.md").write_text("# Outside\n", encoding="utf-8") | ||
| self.write_skill("[Outside](../outside.md)\n") | ||
|
|
||
| errors = validate_agent_skill_links(self.skills) | ||
|
|
||
| self.assertTrue(any("escapes skill root" in error for error in errors)) | ||
|
|
||
| def test_rejects_reference_style_parent_traversal(self) -> None: | ||
| (self.skills / "outside.md").write_text("# Outside\n", encoding="utf-8") | ||
| self.write_skill("[Outside][resource]\n\n[resource]: ../outside.md\n") | ||
|
|
||
| errors = validate_agent_skill_links(self.skills) | ||
|
|
||
| self.assertTrue(any("escapes skill root" in error for error in errors)) | ||
|
|
||
| def test_rejects_absolute_path(self) -> None: | ||
| absolute_file = self.root / "outside.md" | ||
| absolute_file.write_text("# Outside\n", encoding="utf-8") | ||
| self.write_skill(f"[Outside]({absolute_file})\n") | ||
|
|
||
| errors = validate_agent_skill_links(self.skills) | ||
|
|
||
| self.assertTrue(any("absolute local link" in error for error in errors)) | ||
|
|
||
| def test_rejects_directory_target(self) -> None: | ||
| (self.skill / "references").mkdir() | ||
| self.write_skill("[References](references)\n") | ||
|
|
||
| errors = validate_agent_skill_links(self.skills) | ||
|
|
||
| self.assertTrue(any("not a regular file" in error for error in errors)) | ||
|
|
||
| def test_rejects_symlink_target(self) -> None: | ||
| target = self.skill / "details.md" | ||
| target.write_text("# Details\n", encoding="utf-8") | ||
| (self.skill / "linked.md").symlink_to(target) | ||
| self.write_skill("[Details](linked.md)\n") | ||
|
|
||
| errors = validate_agent_skill_links(self.skills) | ||
|
|
||
| self.assertTrue(any("not a regular file" in error for error in errors)) | ||
|
|
||
| def test_rejects_file_uri(self) -> None: | ||
| self.write_skill("[Outside](file:///tmp/outside.md)\n") | ||
|
|
||
| errors = validate_agent_skill_links(self.skills) | ||
|
|
||
| self.assertTrue(any("unsupported URI scheme" in error for error in errors)) | ||
|
|
||
| def test_rejects_unknown_uri_scheme(self) -> None: | ||
| self.write_skill("[Outside](custom://example/resource)\n") | ||
|
|
||
| errors = validate_agent_skill_links(self.skills) | ||
|
|
||
| self.assertTrue(any("unsupported URI scheme" in error for error in errors)) | ||
|
|
||
| def test_rejects_missing_target(self) -> None: | ||
| self.write_skill("[Missing](references/missing.md)\n") | ||
|
|
||
| errors = validate_agent_skill_links(self.skills) | ||
|
|
||
| self.assertTrue(any("missing local resource" in error for error in errors)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| name: Validate Agent Skills | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| pull_request: | ||
| paths: | ||
| - "skills/**" | ||
| - ".github/scripts/validate_agent_skill_links.py" | ||
| - ".github/tests/test_validate_agent_skill_links.py" | ||
| - ".github/workflows/agent-skills.yml" | ||
| push: | ||
| branches: ["main"] | ||
| paths: | ||
| - "skills/**" | ||
| - ".github/scripts/validate_agent_skill_links.py" | ||
| - ".github/tests/test_validate_agent_skill_links.py" | ||
| - ".github/workflows/agent-skills.yml" | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| validate: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
|
|
||
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | ||
| with: | ||
| python-version: "3.13" | ||
|
|
||
| - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 | ||
| with: | ||
| version-file: "python/pyproject.toml" | ||
|
|
||
| - name: Validate Agent Skills specification | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| found_skill=0 | ||
| while IFS= read -r skill_file; do | ||
| [[ -z "$skill_file" ]] && continue | ||
| found_skill=1 | ||
| uvx --from "skills-ref==0.1.1" agentskills validate "$(dirname "$skill_file")" | ||
| done < <(find skills -mindepth 2 -maxdepth 2 -name SKILL.md -print | sort) | ||
|
|
||
| if (( found_skill == 0 )); then | ||
| echo "No public Agent Skills found under skills/." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Validate relative resource links | ||
| run: | | ||
| python .github/tests/test_validate_agent_skill_links.py | ||
| python .github/scripts/validate_agent_skill_links.py skills | ||
|
|
||
| - name: Validate GitHub skill publishing | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: gh skill publish skills --dry-run | ||
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # Microsoft Agent Framework consumer skills | ||
|
|
||
| This directory contains installable Agent Skills for developers building applications with Microsoft Agent Framework. | ||
| These are public distribution artifacts, not contributor instructions for working on the Agent Framework repository. | ||
|
|
||
| ## Available skills | ||
|
|
||
| | Skill | Purpose | | ||
| | --- | --- | | ||
| | `agent-framework-builder-python` | Build, modify, evaluate, integrate, and host Python Agent Framework applications. | | ||
|
|
||
| ## Install | ||
|
|
||
| Preview and install the default-branch version with GitHub CLI: | ||
|
|
||
| ```bash | ||
| gh skill preview microsoft/agent-framework agent-framework-builder-python@main | ||
| gh skill install microsoft/agent-framework agent-framework-builder-python@main | ||
| ``` | ||
|
|
||
| The explicit `@main` ref is required until a tagged Agent Framework release includes the skill. Until then, | ||
| unqualified `gh skill` installs and search discovery can resolve an older release that does not contain it. | ||
|
|
||
| Or use the cross-agent skills CLI: | ||
|
|
||
| ```bash | ||
| npx skills add microsoft/agent-framework --skill agent-framework-builder-python | ||
| ``` | ||
|
|
||
| ## Repository maintenance contract | ||
|
|
||
| - Coding agents working on Agent Framework itself must not treat the skills in this directory as repository | ||
| instructions or automatically load them for normal framework development. | ||
| - Keep the skills outside `.github/skills`, `.agents/skills`, and `.claude/skills` so repository agents do not | ||
| discover them as project skills. | ||
| - Review the Python consumer skill whenever a change affects public APIs, package names or maturity, canonical samples, | ||
| configuration, evaluation, integrations, or hosting guidance. | ||
| - Update only the affected guidance. Do not copy internal contributor conventions into the consumer skill. |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.