Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5004c98
feat(skills): add `azd ai skill` command group (preview)
huimiu May 18, 2026
4757f72
fix(azure.ai.skills): endpoint scheme validation + symlink escape in …
huimiu May 18, 2026
09a750b
fix(skills): use api-version=v1 for skills surface
huimiu May 18, 2026
c957348
fix(skills): pre-check has_blob before download for clearer error
huimiu May 18, 2026
4237ac1
fix(skills): switch from gzip+tar to ZIP for package upload/download
huimiu May 18, 2026
5b20894
fix(skills): auto-detect ZIP vs gzip on download (service is asymmetric)
huimiu May 18, 2026
16a3cb3
refactor(skills): trim unnecessary comments and doc blocks
huimiu May 18, 2026
6f9e6ea
fix(azure.ai.skills): drop unused scanner, stream archive peek, rejec…
huimiu May 18, 2026
3d7bb9a
fix(skills): address PR feedback on download/update help text
huimiu May 19, 2026
1b7af85
Merge branch 'main' into huimiu/hui-add-skill-command
huimiu May 19, 2026
344f564
fix(skills): restore context.go and metadata.go; rename skill_context.go
huimiu May 19, 2026
c9801d7
test(skills): cover archive ext, extract error mapping, delete prefli…
huimiu May 19, 2026
fec6173
fix(skills): use slices.Contains for traversal segment check
huimiu May 19, 2026
ad851b7
fix(skills): address PR feedback - bugs, security hardening, and tests
Copilot May 19, 2026
7e185c9
feat(skills): materialize SKILL.md for blob-less downloads
huimiu May 20, 2026
e8abae7
refactor(skills): rename skill_client.go to skill_context.go
huimiu May 20, 2026
c040787
refactor(skills): align with versioned Skills API spec
huimiu May 26, 2026
3463d4d
fix(skills): address PR review findings
huimiu May 26, 2026
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
9 changes: 9 additions & 0 deletions cli/azd/extensions/azure.ai.skills/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Local test artifacts
SKILL.md
test-skill/
test-min/
test-*/
*.zip
*.tar.gz
azd-ai-skills-*.log
bin/
17 changes: 17 additions & 0 deletions cli/azd/extensions/azure.ai.skills/.golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: "2"

linters:
default: none
enable:
- gosec
- lll
- unused
- errorlint
settings:
lll:
line-length: 220
tab-width: 4

formatters:
enable:
- gofmt
80 changes: 80 additions & 0 deletions cli/azd/extensions/azure.ai.skills/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Azure AI Skills Extension - Agent Instructions

Use this file together with `cli/azd/AGENTS.md`. This guide supplements the root azd instructions with the conventions that are specific to this extension.

## Overview

`azure.ai.skills` is a first-party azd extension under `cli/azd/extensions/azure.ai.skills/`. It runs as a separate Go binary and talks to the azd host over gRPC. It exposes the `azd ai skill <verb>` command group for managing Foundry Skills.

Useful places to start:

- `internal/cmd/`: Cobra commands and top-level orchestration
- `internal/pkg/skill_api/`: typed Foundry Skills REST client, models, SKILL.md parser, and safe ZIP extractor
- `internal/exterrors/`: structured error factories and extension-specific codes

## Relationship to `azure.ai.agents`

This extension is intentionally separate from `azure.ai.agents`. It shares no code symbols but cooperates with it via the global-config endpoint key:

- This extension writes to `extensions.ai-skills.project.context.endpoint` (none yet — read-only today).
- This extension reads `extensions.ai-skills.project.context.endpoint` first, then falls back to `extensions.ai-agents.project.context.endpoint` so users who already configured the endpoint via the agents extension are not forced to re-run `set`.

`AgentCardSkill` (in `azure.ai.agents`) is unrelated to the `Skill` resource managed here and lives in a different Go module.

## Build and test

From `cli/azd/extensions/azure.ai.skills`:

```bash
# Build using developer extension (for local development)
azd x build

# Or build using Go directly
go build
```

If extension work depends on a new azd core change, plan for two PRs:

1. Land the core change in `cli/azd` first.
2. Land the extension change after that, updating this module to the newer azd dependency with `go get github.com/azure/azure-dev/cli/azd && go mod tidy`.

For local development, draft work, or validating both sides together before the core PR is merged, you may temporarily add:

```go
replace github.com/azure/azure-dev/cli/azd => ../../
```

That `replace` points this extension at your local `cli/azd` checkout instead of the version in `go.mod`. Do not merge the extension with that `replace` still present.

## Error handling

This extension uses `internal/exterrors` so the azd host can show a useful message, attach an optional suggestion, and emit stable telemetry. See `cli/azd/extensions/azure.ai.agents/AGENTS.md` "Error handling" section for the full conventions — they apply here unchanged.

Skill-specific error codes live in `internal/exterrors/codes.go`:

- `CodeInvalidSkillName` — name fails the alphanumeric-with-hyphens regex
- `CodeInvalidSkillFile` — SKILL.md front matter unparsable, or `--file` extension unsupported
- `CodeSkillArchiveUnsafe` — `download` rejected an archive entry (zip-slip, symlink, oversized, etc.)
- `CodeSkillOutputCollision` — `download` would overwrite an existing file without `--force`

## Debug logging

Each `--debug` run writes to `azd-ai-skills-<date>.log` in the current working directory. The `skill_api` client deliberately opts out of `IncludeBody` request/response logging until a sanitizer is in place that redacts user-authored `description` and `instructions` fields. Do not enable body logging without that sanitizer.

## File handling

- `--file` is **not** a manifest. It is read at invocation time only; the CLI does not track or re-read it after the command returns.
- `create`: accepts `.md` or `.zip`. Mode is inferred from extension; conflicting modes (inline + `--file`) are rejected. `.md` and inline modes send `inline_content` JSON; `.zip` is uploaded as `multipart/form-data` with a single `files[]` part.
- `update`: accepts `.md` only. `.zip` is rejected with a structured suggestion to use `create --force` (which deletes the skill and all its versions before re-creating). Pass `--set-default-version <ver>` to repoint `default_version` at an existing immutable version without uploading new content.
- `download`: writes either an extracted directory (default) or the unmodified zip archive (`--raw`). Pass `--version <ver>` to download a non-default version. The server always returns `application/zip` (from `GET /skills/{name}/content` or `GET /skills/{name}/versions/{version}/content`).

## Versioning

Skill versions are immutable. The Skill resource itself only carries
`id`, `name`, `description`, `default_version`, `latest_version`, and
`created_at`; per-version content lives in `inline_content` (or uploaded
files) on each `SkillVersion`.

## Release preparation

Follows the same two-PR convention as `azure.ai.agents`: a version-bump PR that touches only `version.txt`, `extension.yaml`, and `CHANGELOG.md`, followed by a registry-update PR generated by `azd x publish` against the released artifacts.
28 changes: 27 additions & 1 deletion cli/azd/extensions/azure.ai.skills/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
# Release History

## 0.0.1-preview - Initial Version
## 0.0.1-preview (Unreleased)

- Initial preview release of the `azure.ai.skills` extension.
- Adds the `azd ai skill` command group on top of the versioned Foundry
Skills API (`Foundry-Features: Skills=V1Preview`):
- `azd ai skill create <name>` — creates a skill and uploads its first
default version via `POST /skills/{name}/versions`. Modes: inline
(`--description` + `--instructions`), SKILL.md file (`--file ./SKILL.md`),
or ZIP package via `multipart/form-data` (`--file ./skill.zip`).
- `azd ai skill update <name>` — uploads a new default version using the
same inline / SKILL.md modes; ZIP is rejected with a pointer to
`create --force`. Pass `--set-default-version <ver>` to repoint
`default_version` at an existing version without uploading new content
(`POST /skills/{name}`).
- `azd ai skill show <name>` — returns `Skill { id, name, description,
default_version, latest_version, created_at }`.
- `azd ai skill list` — paginated, supports `--top` and `--orderby`.
- `azd ai skill download <name>` — downloads the zip content from
`GET /skills/{name}/content`, or `GET /skills/{name}/versions/{version}/content`
when `--version` is passed. Extracts into `./.agents/skills/<name>/` by
default; `--raw` writes the unmodified zip archive.
- `azd ai skill delete <name>` — confirmation by default, `--force` to skip.
- Skill names follow the agentskills.io spec:
`^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$`, max 64 chars (lowercase only).
- Shares the Foundry project-endpoint resolution cascade with `azure.ai.agents`,
reading `extensions.ai-skills.project.context.endpoint` first and falling
back to `extensions.ai-agents.project.context.endpoint`.
83 changes: 81 additions & 2 deletions cli/azd/extensions/azure.ai.skills/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,82 @@
# Foundry Skills
# Azure Developer CLI (azd) Skills Extension

Manage Microsoft Foundry Skills from your terminal. (Preview)
Manage [Microsoft Foundry](https://learn.microsoft.com/azure/ai-services/) **skills**
(reusable behavioral guidelines an agent can attach at runtime) directly from your
terminal.

## Commands

```bash
azd ai skill create <name> [--description "..." --instructions "..."]
azd ai skill create <name> --file ./SKILL.md
azd ai skill create <name> --file ./skill.zip

Comment thread
huimiu marked this conversation as resolved.
azd ai skill update <name> [--description "..."] [--instructions "..."] [--file ./SKILL.md]
azd ai skill update <name> --set-default-version <version>
azd ai skill show <name>
azd ai skill list [--top N] [--orderby <field>]
azd ai skill download <name> [--version <ver>] [--output-dir <path>] [--raw] [--force]
azd ai skill delete <name> [--force]
```

Skills are **versioned and immutable**. `create` uploads the first default
version; `update` uploads a new default version (or, with
`--set-default-version`, just repoints `default_version` at an existing
version). Names follow the agentskills.io spec
(`^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$`, max 64 chars).

All commands accept the standard cross-cutting flags: `-p` / `--project-endpoint`,
`--output table|json`, `--no-prompt`, and `--debug`.

## Project endpoint resolution

The Foundry project endpoint is resolved in this order:

1. `-p` / `--project-endpoint` flag on the command.
2. Active azd env value `AZURE_AI_PROJECT_ENDPOINT`.
3. Global config `extensions.ai-skills.project.context.endpoint`
(falls back to `extensions.ai-agents.project.context.endpoint` so users who
configured the endpoint via the agents extension are not forced to re-run `set`).
4. Host environment variable `FOUNDRY_PROJECT_ENDPOINT`.
5. Structured error with an actionable suggestion.

## Local Development

### Prerequisites

1. **Install developer kit extension** (if not already installed):

```bash
azd ext install microsoft.azd.extensions
```

### Building and installing locally

1. **Navigate to the extension directory**:

```bash
cd cli/azd/extensions/azure.ai.skills
```

2. **Initial setup** (first time only):

```bash
azd x build
azd x pack
azd x publish
```

3. **Install the extension**:

```bash
azd ext install azure.ai.skills
```

4. **For subsequent development** (after initial setup):

```bash
azd x watch
```

This automatically watches for file changes, rebuilds, and installs updates
locally.
4 changes: 2 additions & 2 deletions cli/azd/extensions/azure.ai.skills/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ else {
)
}

$APP_PATH = "$env:EXTENSION_ID/internal/cmd"
$VERSION_PATH = "azureaiskills/internal/version"

# Loop through platforms and build
foreach ($PLATFORM in $PLATFORMS) {
Expand All @@ -65,7 +65,7 @@ foreach ($PLATFORM in $PLATFORMS) {
$env:GOARCH = $ARCH

go build `
-ldflags="-X '$APP_PATH.Version=$env:EXTENSION_VERSION' -X '$APP_PATH.Commit=$COMMIT' -X '$APP_PATH.BuildDate=$BUILD_DATE'" `
-ldflags="-X '$VERSION_PATH.Version=$env:EXTENSION_VERSION' -X '$VERSION_PATH.Commit=$COMMIT' -X '$VERSION_PATH.BuildDate=$BUILD_DATE'" `
-o $OUTPUT_NAME

if ($LASTEXITCODE -ne 0) {
Expand Down
4 changes: 2 additions & 2 deletions cli/azd/extensions/azure.ai.skills/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ else
)
fi

APP_PATH="$EXTENSION_ID/internal/cmd"
VERSION_PATH="azureaiskills/internal/version"

# Loop through platforms and build
for PLATFORM in "${PLATFORMS[@]}"; do
Expand All @@ -53,7 +53,7 @@ for PLATFORM in "${PLATFORMS[@]}"; do

# Set environment variables for Go build
GOOS=$OS GOARCH=$ARCH go build \
-ldflags="-X '$APP_PATH.Version=$EXTENSION_VERSION' -X '$APP_PATH.Commit=$COMMIT' -X '$APP_PATH.BuildDate=$BUILD_DATE'" \
-ldflags="-X '$VERSION_PATH.Version=$EXTENSION_VERSION' -X '$VERSION_PATH.Commit=$COMMIT' -X '$VERSION_PATH.BuildDate=$BUILD_DATE'" \
-o "$OUTPUT_NAME"

if [ $? -ne 0 ]; then
Expand Down
37 changes: 35 additions & 2 deletions cli/azd/extensions/azure.ai.skills/ci-build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ param(
[string] $MSYS2Shell, # path to msys2_shell.cmd
[string] $OutputFileName
)

$PSNativeCommandArgumentPassing = 'Legacy'

# Remove any previously built binaries
Expand All @@ -19,24 +18,50 @@ if ($LASTEXITCODE) {

# Run `go help build` to obtain detailed information about `go build` flags.
$buildFlags = @(
# remove all file system paths from the resulting executable.
# Instead of absolute file system paths, the recorded file names
# will begin either a module path@version (when using modules),
# or a plain import path (when using the standard library, or GOPATH).
"-trimpath",

# Use buildmode=pie (Position Independent Executable) for enhanced security across platforms
# against memory corruption exploits across all major platforms.
#
# On Windows, the -buildmode=pie flag enables Address Space Layout
# Randomization (ASLR) and automatically sets DYNAMICBASE and HIGH-ENTROPY-VA flags in the PE header.
"-buildmode=pie"
)

if ($CodeCoverageEnabled) {
$buildFlags += "-cover"
}

# Build constraint tags
# cfi: Enable Control Flow Integrity (CFI),
# cfg: Enable Control Flow Guard (CFG),
# osusergo: Optimize for OS user accounts
$tagsFlag = "-tags=cfi,cfg,osusergo"

$ldFlag = "-ldflags=-s -w -X azure.ai.skills/internal/cmd.Version=$Version -X azure.ai.skills/internal/cmd.Commit=$SourceVersion -X azure.ai.skills/internal/cmd.BuildDate=$(Get-Date -Format o) "
# ld linker flags
# -s: Omit symbol table and debug information
# -w: Omit DWARF symbol table
# -X: Set variable at link time. Used to set the version in source.

$ldFlag = "-ldflags=-s -w -X 'azureaiskills/internal/version.Version=$Version' -X 'azureaiskills/internal/version.Commit=$SourceVersion' -X 'azureaiskills/internal/version.BuildDate=$(Get-Date -Format o)' "

if ($IsWindows) {
$msg = "Building for Windows"
Write-Host $msg
}
elseif ($IsLinux) {
Write-Host "Building for linux"

# Disable cgo in the x64 Linux build. This will also statically
# link the resulting binary which increases backwards
# compatibility with older versions of Linux.
if ($env:GOARCH -ne "arm64") {
$env:CGO_ENABLED = "0"
}
}
elseif ($IsMacOS) {
Write-Host "Building for macOS"
Expand All @@ -57,13 +82,17 @@ function PrintFlags() {
[string] $flags
)

# Attempt to format flags so that they are easily copy-pastable to be ran inside pwsh
$i = 0
foreach ($buildFlag in $buildFlags) {
# If the flag has a value, wrap it in quotes. This is not required when invoking directly below,
# but when repasted into a shell for execution, the quotes can help escape special characters such as ','.
$argWithValue = $buildFlag.Split('=', 2)
if ($argWithValue.Length -eq 2 -and !$argWithValue[1].StartsWith("`"")) {
$buildFlag = "$($argWithValue[0])=`"$($argWithValue[1])`""
}

# Write each flag on a newline with '`' acting as the multiline separator
if ($i -eq $buildFlags.Length - 1) {
Write-Host " $buildFlag"
}
Expand All @@ -75,6 +104,8 @@ function PrintFlags() {
}

$oldGOEXPERIMENT = $env:GOEXPERIMENT
# Enable the loopvar experiment, which makes the loop variaible for go loops like `range` behave as most folks would expect.
# the go team is exploring making this default in the future, and we'd like to opt into the behavior now.
$env:GOEXPERIMENT = "loopvar"

try {
Expand All @@ -87,6 +118,7 @@ try {
}

if ($BuildRecordMode) {
# Modify build tags to include record
$recordTagPatched = $false
for ($i = 0; $i -lt $buildFlags.Length; $i++) {
if ($buildFlags[$i].StartsWith("-tags=")) {
Expand All @@ -97,6 +129,7 @@ try {
if (-not $recordTagPatched) {
$buildFlags += "-tags=record"
}
# Add output file flag for record mode
$recordOutput = "-o=$OutputFileName-record"
if ($IsWindows) { $recordOutput += ".exe" }
$buildFlags += $recordOutput
Expand Down
16 changes: 15 additions & 1 deletion cli/azd/extensions/azure.ai.skills/cspell.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,16 @@
import: ../../.vscode/cspell.yaml
words: []
words:
# Skill commands
- agentskills
- azureaiskills
- exterrors
- foundry
- foundrysdk
- orderby
- repoint
- repoints
- tarball
- zipslip
- gzip
- skill
- skills
Loading
Loading