Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@

# .NET owners
/dotnet/ @eavanvalkenburg @SergeyMenshykh @rogerbarreto @westey-m @moonbox3 @baywet @peibekwe

# Public Python consumer skill
/skills/agent-framework-builder-python/ @eavanvalkenburg @SergeyMenshykh @rogerbarreto @westey-m @moonbox3 @jpalvarezl @sophia-ramsey @TaoChenOSU @giles17
13 changes: 13 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ Microsoft Agent Framework - a multi-language framework for building, orchestrati
- `python/` - Python implementation → see [python/AGENTS.md](../python/AGENTS.md)
- `dotnet/` - C#/.NET implementation → see [dotnet/AGENTS.md](../dotnet/AGENTS.md)
- `docs/` - Design documents and architectural decision records
- `skills/` - Public, installable Agent Skills for developers using Agent Framework

## Public Consumer Skills

The skills under `skills/` are distribution artifacts for developers building Agent Framework applications. They are
not project skills for contributors working in this repository:

- Do not load or invoke them as instructions for normal Agent Framework repository work.
- Keep them outside `.github/skills`, `.agents/skills`, and `.claude/skills` so coding agents do not auto-discover them.
- When a change affects public APIs, packages, samples, configuration, evaluation, integrations, or hosting guidance,
review and update the corresponding consumer skill as maintained product documentation.
- Read a consumer skill during repository work only when that maintenance check is relevant; do not let its standalone
application defaults override repository-specific instructions.

## Architectural Decision Records (ADRs)

Expand Down
87 changes: 87 additions & 0 deletions .github/scripts/validate_agent_skill_links.py
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()
123 changes: 123 additions & 0 deletions .github/tests/test_validate_agent_skill_links.py
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()
62 changes: 62 additions & 0 deletions .github/workflows/agent-skills.yml
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
Comment thread
eavanvalkenburg marked this conversation as resolved.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Explore new MAF capabilities and real implementation patterns on the [official b
- [Quickstart](#quickstart)
- [Basic Agent - Python](#basic-agent---python)
- [Basic Agent - .NET](#basic-agent---net)
- [Agent Skills for Coding Agents](#agent-skills-for-coding-agents)
- [More Examples & Samples](#more-examples--samples)
- [Community & Feedback](#community--feedback)
- [Troubleshooting](#troubleshooting)
Expand Down Expand Up @@ -161,6 +162,25 @@ AIAgent agent =
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
```

## Agent Skills for Coding Agents

Install the Python Agent Framework builder skill to give a compatible coding agent current guidance for choosing and
building agents, harnesses, workflows, evaluations, integrations, and hosting:

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

The cross-agent `skills` CLI is also supported:

```bash
npx skills add microsoft/agent-framework --skill agent-framework-builder-python
```

See the [consumer skills catalog](./skills/) for details.

## More Examples & Samples

### Python
Expand Down
38 changes: 38 additions & 0 deletions skills/README.md
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.
Loading
Loading