Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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.
18 changes: 17 additions & 1 deletion cli/azd/extensions/azure.ai.skills/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# 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 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`.
76 changes: 74 additions & 2 deletions cli/azd/extensions/azure.ai.skills/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,75 @@
# 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 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.
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
39 changes: 36 additions & 3 deletions cli/azd/extensions/azure.ai.skills/ci-build.ps1
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
param(
[string] $Version = (Get-Content "$PSScriptRoot/version.txt"),
[string] $Version = (Get-Content "$PSScriptRoot/../version.txt"),
[string] $SourceVersion = (git rev-parse HEAD),
[switch] $CodeCoverageEnabled,
[switch] $BuildRecordMode,
[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
13 changes: 12 additions & 1 deletion cli/azd/extensions/azure.ai.skills/cspell.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
import: ../../.vscode/cspell.yaml
words: []
words:
# Skill commands
- azureaiskills
- exterrors
- foundry
- foundrysdk
- orderby
- tarball
- zipslip
- gzip
- skill
- skills
32 changes: 22 additions & 10 deletions cli/azd/extensions/azure.ai.skills/extension.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/refs/heads/main/cli/azd/extensions/extension.schema.json
capabilities:
- custom-commands
- metadata
description: Manage Microsoft Foundry Skills from your terminal. (Preview)
displayName: Foundry Skills (Preview)
# yaml-language-server: $schema=../extension.schema.json
id: azure.ai.skills
language: go
namespace: ai.skill
tags:
- ai
- skill
displayName: Foundry skills (Preview)
description: Manage Microsoft Foundry skills (reusable agent behavioral guidelines) from your terminal. (Preview)
usage: azd ai skill <command> [options]
# NOTE: Make sure version.txt is in sync with this version.
version: 0.0.1-preview
requiredAzdVersion: ">1.23.13"
language: go
capabilities:
- custom-commands
- metadata
tags:
- ai
- skill
examples:
- name: list
description: List skills in the current Foundry project.
usage: azd ai skill list
- name: create
description: Create a skill from a SKILL.md file.
usage: azd ai skill create my-skill --file ./SKILL.md
- name: download
description: Download and extract a skill into ./.agents/skills/.
usage: azd ai skill download my-skill
Loading
Loading