Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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