Skip to content
Open
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
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