Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
73 changes: 73 additions & 0 deletions cli/azd/extensions/azure.ai.skills/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 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.
- `update`: accepts `.md` only. `.zip` is rejected with a structured suggestion to use `create --force`.
- `download`: writes either an extracted directory (default) or the unmodified ZIP archive (`--raw`).

## 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.
19 changes: 19 additions & 0 deletions cli/azd/extensions/azure.ai.skills/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Release History

## 0.0.1-preview (Unreleased)

- Initial preview release of the `azure.ai.skills` extension.
- Adds the `azd ai skill` command group with full CRUD over Foundry Skills:
- `azd ai skill create <name>` — inline (`--description` + `--instructions`),
SKILL.md file (`--file ./SKILL.md`), or ZIP package (`--file ./skill.zip`).
- `azd ai skill update <name>` — inline or `--file *.md`.
- `azd ai skill show <name>` — metadata only.
- `azd ai skill list` — paginated, supports `--top` and `--orderby`.
- `azd ai skill download <name>` — extracts to `./.agents/skills/<name>/` by
default; `--raw` keeps the archive as-is. The downloader auto-detects ZIP
vs gzip-tar via magic bytes because the Foundry surface is asymmetric:
uploads require `application/zip`, downloads return `application/gzip`.
- `azd ai skill delete <name>` — confirmation by default, `--force` to skip.
- 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`.
75 changes: 75 additions & 0 deletions cli/azd/extensions/azure.ai.skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Azure Developer CLI (azd) Skills Extension

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 show <name>
azd ai skill list [--top N] [--orderby <field>]
azd ai skill download <name> [--output-dir <path>] [--raw] [--force]
azd ai skill delete <name> [--force]
```

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.
78 changes: 78 additions & 0 deletions cli/azd/extensions/azure.ai.skills/build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Ensure script fails on any error
$ErrorActionPreference = 'Stop'

# Get the directory of the script
$EXTENSION_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path

# Change to the script directory
Set-Location -Path $EXTENSION_DIR

# Create a safe version of EXTENSION_ID replacing dots with dashes
$EXTENSION_ID_SAFE = $env:EXTENSION_ID -replace '\.', '-'

# Define output directory
$OUTPUT_DIR = if ($env:OUTPUT_DIR) { $env:OUTPUT_DIR } else { Join-Path $EXTENSION_DIR "bin" }

# Create output directory if it doesn't exist
if (-not (Test-Path -Path $OUTPUT_DIR)) {
New-Item -ItemType Directory -Path $OUTPUT_DIR | Out-Null
}

# Get Git commit hash and build date
$COMMIT = git rev-parse HEAD
if ($LASTEXITCODE -ne 0) {
Write-Host "Error: Failed to get git commit hash"
exit 1
}
$BUILD_DATE = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")

# List of OS and architecture combinations
if ($env:EXTENSION_PLATFORM) {
$PLATFORMS = @($env:EXTENSION_PLATFORM)
}
else {
$PLATFORMS = @(
"windows/amd64",
"windows/arm64",
"darwin/amd64",
"darwin/arm64",
"linux/amd64",
"linux/arm64"
)
}

$VERSION_PATH = "azureaiskills/internal/version"

# Loop through platforms and build
foreach ($PLATFORM in $PLATFORMS) {
$OS, $ARCH = $PLATFORM -split '/'

$OUTPUT_NAME = Join-Path $OUTPUT_DIR "$EXTENSION_ID_SAFE-$OS-$ARCH"

if ($OS -eq "windows") {
$OUTPUT_NAME += ".exe"
}

Write-Host "Building for $OS/$ARCH..."

# Delete the output file if it already exists
if (Test-Path -Path $OUTPUT_NAME) {
Remove-Item -Path $OUTPUT_NAME -Force
}

# Set environment variables for Go build
$env:GOOS = $OS
$env:GOARCH = $ARCH

go build `
-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) {
Write-Host "An error occurred while building for $OS/$ARCH"
exit 1
}
}

Write-Host "Build completed successfully!"
Write-Host "Binaries are located in the $OUTPUT_DIR directory."
66 changes: 66 additions & 0 deletions cli/azd/extensions/azure.ai.skills/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/bin/bash

# Get the directory of the script
EXTENSION_DIR="$(cd "$(dirname "$0")" && pwd)"

# Change to the script directory
cd "$EXTENSION_DIR" || exit

# Create a safe version of EXTENSION_ID replacing dots with dashes
EXTENSION_ID_SAFE="${EXTENSION_ID//./-}"

# Define output directory
OUTPUT_DIR="${OUTPUT_DIR:-$EXTENSION_DIR/bin}"

# Create output and target directories if they don't exist
mkdir -p "$OUTPUT_DIR"

# Get Git commit hash and build date
COMMIT=$(git rev-parse HEAD)
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# List of OS and architecture combinations
if [ -n "$EXTENSION_PLATFORM" ]; then
PLATFORMS=("$EXTENSION_PLATFORM")
else
PLATFORMS=(
"windows/amd64"
"windows/arm64"
"darwin/amd64"
"darwin/arm64"
"linux/amd64"
"linux/arm64"
)
fi

VERSION_PATH="azureaiskills/internal/version"

# Loop through platforms and build
for PLATFORM in "${PLATFORMS[@]}"; do
OS=$(echo "$PLATFORM" | cut -d'/' -f1)
ARCH=$(echo "$PLATFORM" | cut -d'/' -f2)

OUTPUT_NAME="$OUTPUT_DIR/$EXTENSION_ID_SAFE-$OS-$ARCH"

if [ "$OS" = "windows" ]; then
OUTPUT_NAME+='.exe'
fi

echo "Building for $OS/$ARCH..."

# Delete the output file if it already exists
[ -f "$OUTPUT_NAME" ] && rm -f "$OUTPUT_NAME"

# Set environment variables for Go build
GOOS=$OS GOARCH=$ARCH go build \
-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
echo "An error occurred while building for $OS/$ARCH"
exit 1
fi
done

echo "Build completed successfully!"
echo "Binaries are located in the $OUTPUT_DIR directory."
Loading