Skip to content

feat(skills): add azd ai skill command group#8224

Open
huimiu wants to merge 14 commits into
mainfrom
huimiu/hui-add-skill-command
Open

feat(skills): add azd ai skill command group#8224
huimiu wants to merge 14 commits into
mainfrom
huimiu/hui-add-skill-command

Conversation

@huimiu
Copy link
Copy Markdown
Member

@huimiu huimiu commented May 18, 2026

Closes #8142. Implements the design from #8204.

Summary

New standalone azure.ai.skills extension exposing azd ai skill create | update | show | list | download | delete.

  • create — three mutually exclusive modes: inline (--description + --instructions), SKILL.md (--file *.md), or gzip package (--file *.zip). --force does delete-then-create.
  • update — inline flags or --file *.md only; gzip rejected with suggestion to use create --force.
  • download — default extracts into ./.agents/skills/<name>/ with two-phase safe extraction (zip-slip guard, no symlinks, 10K-entry / 512 MB caps). --raw keeps the gzip archive.
  • delete — confirms by default; --force skips, --no-prompt requires --force.
  • Endpoint cascade shares state with azure.ai.agents via read-only fallback to extensions.ai-agents.project.context.endpoint.

Test

  • Install ai.skill extension
    image

  • Create skill
    image

  • Create skill (create --description X --file Y)
    image

  • Create skill (zip)
    image

  • Show skill
    image

  • List skills
    image

  • Download skill (no-blob)
    image

  • Download skill
    image

  • Update skill
    image

  • Delete skill (interactive)
    image

  • Delete skill (--no-prompt, without --force)
    image

  • Delete skill
    image

Introduces a new standalone `azure.ai.skills` extension that exposes
`azd ai skill create | update | show | list | download | delete` for
managing Foundry Skills from any directory.

Implements the design in PR #8204 / issue #8142:

- New extension under `cli/azd/extensions/azure.ai.skills` (namespace
  `ai.skill`, id `azure.ai.skills`, version `0.0.1-preview`).
- Typed Foundry Skills data-plane client in `internal/pkg/skill_api`
  with SKILL.md YAML front matter parser and two-phase safe gzip-tar
  extractor (zip-slip guard, no symlinks / hard links, 10,000-entry /
  512 MB caps, staging + atomic copy, `--force` for collisions).
- Three mutually exclusive `create` modes: inline (`--description` +
  `--instructions`), `--file SKILL.md` (parsed locally), and
  `--file *.tar.gz` / `*.tgz` (streamed as `application/gzip` to
  `POST /skills:import`). `--force` does delete-then-create.
- `update` accepts inline flags or `--file *.md` only; gzip is
  rejected with a structured suggestion to use `create --force`.
- `download` extracts by default into `./.agents/skills/<name>/` and
  supports `--raw` to write the unmodified archive.
- `delete` confirms by default; `--force` skips, and `--no-prompt`
  without `--force` errors. Interactive `n` returns exit 0.
- Endpoint resolution shares the 5-level cascade with `azure.ai.agents`
  via a read-only fallback to `extensions.ai-agents.project.context.endpoint`
  when the new `extensions.ai-skills.project.context.endpoint` key is
  unset.
- Bearer scope `https://ai.azure.com/.default`, `Foundry-Features:
  Skills=V1Preview` header, API version `2025-11-15-preview`. Debug
  log file `azd-ai-skills-<date>.log`. `IncludeBody` opt-out on the
  HTTP pipeline until a sanitizer for `description` / `instructions`
  lands.
- Skill name regex aligned with `agent_yaml.ValidateAgentName`:
  `^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\$`.

Closes #8142.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@huimiu huimiu changed the title feat(skills): add azd ai skill command group (preview) feat(skills): add azd ai skill command group May 18, 2026
@huimiu huimiu force-pushed the huimiu/hui-add-skill-command branch from 77c69a9 to 28ca3b2 Compare May 18, 2026 07:39
…SafeExtract

P1 - Endpoint validation (Finding 1):
- Add validateEndpoint() to endpoint.go that rejects non-https URLs and
  empty hosts before sending Azure bearer tokens to any resolved endpoint.
- Called at all five resolution levels (flag, azd env, global config x2,
  host env var) so misconfigured endpoints fail with a clear message rather
  than an opaque SDK error.
- Host-suffix validation intentionally deferred to HTTP layer per design.
- Add TestResolveProjectEndpoint_InvalidScheme covering http/ftp/no-scheme/empty-host cases.

P1 - Symlink escape in SafeExtract (Finding 2):
- archive.go copy phase was vulnerable: if OutputDir contained a pre-existing
  symlink to a directory outside OutputDir, MkdirAll + copyFile would follow
  it silently, writing extracted files outside the intended destination.
- Fix: resolve OutputDir with EvalSymlinks once; before each copyFile resolve
  the destination directory and assert it is under the real output dir.
- Add isUnder() helper for path containment check.
- Add TestSafeExtract_RejectsSymlinkParentEscape (skipped on Windows).
@huimiu huimiu force-pushed the huimiu/hui-add-skill-command branch from 28ca3b2 to 4757f72 Compare May 18, 2026 07:43
huimiu and others added 2 commits May 18, 2026 15:58
The Skills data-plane lives under api-version=v1; the preview opt-in is
communicated via the Foundry-Features: Skills=V1Preview header. Using
2025-11-15-preview as the api-version returns 400 UnsupportedApiVersion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Skills created from inline JSON or SKILL.md have no downloadable
package; the server returns an opaque 404 'does not have an associated
package'. Pre-flight Get the skill so the download command can return a
structured CodeSkillNoPackage validation error with an actionable
suggestion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@huimiu huimiu force-pushed the huimiu/hui-add-skill-command branch from 256aec8 to c957348 Compare May 18, 2026 08:06
huimiu and others added 2 commits May 18, 2026 16:22
The live Foundry Skills service implements POST /skills:import and
GET /skills/{name}:download with application/zip, not application/gzip
as the upstream TypeSpec declares. Verified via 415 Unsupported Media
Type on gzip uploads. Public docs confirm:
https://learn.microsoft.com/azure/foundry/agents/how-to/tools/skills

Changes:
- skill_api: replace archive/tar+compress/gzip with archive/zip
- skill_api: Download now returns []byte (archive/zip needs io.ReaderAt)
- skill_api: rename ContentTypeGzip -> ContentTypeZip, ErrInvalidGzip ->
  ErrInvalidZip
- cmd: accept '.zip' for --file; reject '.tar.gz'/'.tgz'
- cmd: writeRaw now writes '<name>.zip'
- tests: rewrite archive_test.go and archive_peek_test.go for ZIP
- docs (AGENTS.md, README.md, CHANGELOG.md): s/gzip,tar.gz/zip/g

The design spec (PR #8204) will need a follow-up to match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Foundry Skills surface is asymmetric on archive format:
- POST /skills:import requires application/zip (gzip yields 415)
- GET  /skills/{name}:download returns application/gzip

Make SafeExtract sniff magic bytes (PK or 1f 8b) and dispatch to either
the zip or the gzip+tar handler. Download() now accepts both
Content-Type values. The raw download filename uses .zip or .tar.gz
based on the detected format.

Also:
- Add DetectArchiveFormat() public helper for callers that need the
  format (e.g. the --raw filename picker).
- Add gitignore for local test artifacts (zips, tar.gz, debug logs).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@huimiu huimiu marked this pull request as ready for review May 18, 2026 09:04
Copilot AI review requested due to automatic review settings May 18, 2026 09:04
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 18, 2026

📋 Prioritization Note

Thanks for the contribution! The linked issue isn't in the current milestone yet.
Review may take a bit longer — reach out to @rajeshkamal5050 or @kristenwomack if you'd like to discuss prioritization.

Remove restating-the-code comments, narrative section headers, and
verbose struct-field docs. Keep design-rationale notes (TypeSpec/docs
mismatch on archive format, --force destructive sequence, defensive
path validation) and important safety annotations.

Net: -396 lines of comments, no behavior change.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new first-party azure.ai.skills azd extension that exposes the standalone azd ai skill command group for managing Foundry Skills from the terminal, including endpoint resolution, typed REST client calls, SKILL.md parsing, and safe package download/extraction.

Changes:

  • Introduces the azd ai skill Cobra command tree (create|update|show|list|download|delete) with structured errors and output formatting.
  • Adds a typed Skills data-plane REST client plus SKILL.md parsing and archive handling helpers (including “safe extract”).
  • Adds extension packaging/build assets (extension.yaml, versioning, build scripts, lint config, and docs).

Reviewed changes

Copilot reviewed 42 out of 43 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
cli/azd/extensions/azure.ai.skills/version.txt Initial extension version.
cli/azd/extensions/azure.ai.skills/README.md Extension overview, command surface, endpoint resolution, dev workflow.
cli/azd/extensions/azure.ai.skills/main.go Extension binary entry point.
cli/azd/extensions/azure.ai.skills/internal/version/version.go Build-time version variables.
cli/azd/extensions/azure.ai.skills/internal/pkg/skill_api/skill_md.go SKILL.md YAML-front-matter + body parser.
cli/azd/extensions/azure.ai.skills/internal/pkg/skill_api/skill_md_test.go Unit tests for SKILL.md parsing.
cli/azd/extensions/azure.ai.skills/internal/pkg/skill_api/models.go Public/CLI JSON models + wire conversions.
cli/azd/extensions/azure.ai.skills/internal/pkg/skill_api/client.go Typed REST client for Skills (create/update/get/list/download/delete).
cli/azd/extensions/azure.ai.skills/internal/pkg/skill_api/client_test.go Client request/response behavior tests via httptest server.
cli/azd/extensions/azure.ai.skills/internal/pkg/skill_api/archive.go Archive format detection + safe extraction implementation.
cli/azd/extensions/azure.ai.skills/internal/pkg/skill_api/archive_test.go Tests for safe extraction and validation limits.
cli/azd/extensions/azure.ai.skills/internal/pkg/skill_api/archive_peek.go ZIP SKILL.md name peek helper for --force safety checks.
cli/azd/extensions/azure.ai.skills/internal/pkg/skill_api/archive_peek_test.go Tests for archive name peek behavior.
cli/azd/extensions/azure.ai.skills/internal/exterrors/errors.go Structured error helpers and Azure SDK error conversion.
cli/azd/extensions/azure.ai.skills/internal/exterrors/codes.go Extension-specific error codes and operation names.
cli/azd/extensions/azure.ai.skills/internal/cmd/version.go version subcommand.
cli/azd/extensions/azure.ai.skills/internal/cmd/skill_validate.go Skill name validation.
cli/azd/extensions/azure.ai.skills/internal/cmd/skill_validate_test.go Tests for name validation + flag-mode validation helpers.
cli/azd/extensions/azure.ai.skills/internal/cmd/skill_update.go skill update implementation (GET-merge-POST).
cli/azd/extensions/azure.ai.skills/internal/cmd/skill_show.go skill show implementation.
cli/azd/extensions/azure.ai.skills/internal/cmd/skill_list.go skill list implementation (paged flattening).
cli/azd/extensions/azure.ai.skills/internal/cmd/skill_download.go skill download implementation (raw vs extracted).
cli/azd/extensions/azure.ai.skills/internal/cmd/skill_delete.go skill delete implementation with confirmation behavior.
cli/azd/extensions/azure.ai.skills/internal/cmd/skill_create.go skill create implementation (inline, SKILL.md, ZIP package).
cli/azd/extensions/azure.ai.skills/internal/cmd/skill_context.go Endpoint + credential + client construction.
cli/azd/extensions/azure.ai.skills/internal/cmd/root.go Root command wiring, persistent flags, and host commands.
cli/azd/extensions/azure.ai.skills/internal/cmd/output.go JSON/table output helpers for skills.
cli/azd/extensions/azure.ai.skills/internal/cmd/output_test.go Tests for table output rendering.
cli/azd/extensions/azure.ai.skills/internal/cmd/endpoint.go Endpoint resolution cascade + URL validation.
cli/azd/extensions/azure.ai.skills/internal/cmd/endpoint_test.go Tests for endpoint resolution and validation.
cli/azd/extensions/azure.ai.skills/internal/cmd/debug.go Debug logging setup for stdlib log + Azure SDK logger.
cli/azd/extensions/azure.ai.skills/go.sum Extension module dependency lockfile.
cli/azd/extensions/azure.ai.skills/go.mod Extension module definition and dependencies.
cli/azd/extensions/azure.ai.skills/extension.yaml Extension metadata and examples for azd.
cli/azd/extensions/azure.ai.skills/cspell.yaml Spellcheck configuration for extension-specific terms.
cli/azd/extensions/azure.ai.skills/ci-test.ps1 CI test runner for the extension.
cli/azd/extensions/azure.ai.skills/ci-build.ps1 CI build script for the extension binary.
cli/azd/extensions/azure.ai.skills/CHANGELOG.md Initial release notes for the extension.
cli/azd/extensions/azure.ai.skills/build.sh Local multi-platform build helper (bash).
cli/azd/extensions/azure.ai.skills/build.ps1 Local multi-platform build helper (PowerShell).
cli/azd/extensions/azure.ai.skills/AGENTS.md Extension-specific contributor/agent guidance.
cli/azd/extensions/azure.ai.skills/.golangci.yaml Extension-local golangci-lint configuration.
cli/azd/extensions/azure.ai.skills/.gitignore Ignores local build/test artifacts for the extension.
Comments suppressed due to low confidence (3)

cli/azd/extensions/azure.ai.skills/internal/pkg/skill_api/archive.go:389

  • copyFile writes to dst via os.OpenFile(..., O_TRUNC, ...), which will follow a symlink if one exists at dst. For a “safe extraction” helper, this should defensively refuse to write through symlinks (and other non-regular file types), including in --force mode.
    cli/azd/extensions/azure.ai.skills/internal/cmd/skill_download.go:287
  • The command help text says download retrieves a “ZIP package” and --raw writes the “ZIP archive as-is”, but the implementation explicitly supports ZIP or gzip-tar downloads (and even chooses the extension based on magic bytes). Update the Long/flag help strings to refer to a generic “archive/package” to avoid contradicting actual behavior.
    cli/azd/extensions/azure.ai.skills/internal/cmd/endpoint.go:133
  • validateEndpoint returns plain fmt.Errorf errors for invalid user input. Since endpoint validation errors are user-fixable and should produce stable telemetry, consider returning a structured exterrors.Validation (e.g., CodeInvalidParameter) instead of an untyped error so the azd host categorizes and renders it consistently.

Comment thread cli/azd/extensions/azure.ai.skills/internal/cmd/skill_create.go
Comment thread cli/azd/extensions/azure.ai.skills/README.md
Comment thread cli/azd/extensions/azure.ai.skills/internal/pkg/skill_api/models.go
Copy link
Copy Markdown
Member

@jongio jongio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean extension structure - follows the azure.ai.agents patterns well, archive security is solid (zip-slip guard, symlink rejection, staging directory, size caps), and the test coverage across client/archive/parser/validation is thorough.

One nit on Go 1.26 style below. The Copilot bot's comment about the unused bufio.Scanner in findFrontMatterBounds is valid too - that variable gets .Buffer() called so it compiles, but it's never actually used for scanning.

Comment thread cli/azd/extensions/azure.ai.skills/internal/cmd/skill_create.go
…t symlinked copy destinations, use errors.AsType
Copy link
Copy Markdown
Member

@trangevi trangevi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add tests where possible

Comment thread cli/azd/extensions/azure.ai.skills/internal/cmd/skill_download.go Outdated
Comment thread cli/azd/extensions/azure.ai.skills/internal/cmd/skill_download.go
},
}

cmd.Flags().IntVar(&flags.top, "top", 0, "Return up to N skills (default: all)")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better name we can give this? Or we think it's a common enough thing that it won't be weird to people?

@therealjohn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove these flags for now? I don't see how top or orderby is useful without more support to query for exactly what you'd want. We can capture filtering as a separate feature to think more on what it needs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed --top and --orderby flags from skill list in the latest commit. The command now always fetches all pages transparently.

Comment thread cli/azd/extensions/azure.ai.skills/internal/cmd/skill_update.go Outdated
Comment thread cli/azd/extensions/azure.ai.skills/internal/cmd/skill_update.go Outdated
- skill_download: drop 're-create with create --force' hint from the no-package error; users downloading the skill don't have it locally to re-create.
- skill_update: in 'update' long help, route ZIP package updates to 'create --force' and call out that skills are not versioned so it's a destructive (delete-then-recreate) path.
@huimiu huimiu force-pushed the huimiu/hui-add-skill-command branch from 83d3aa4 to 3d7bb9a Compare May 19, 2026 06:42
Resolve conflicts in cli/azd/extensions/azure.ai.skills/.
- Keep PR's full `azd ai skill` command-group implementation, build scripts, CHANGELOG and README.
- Adopt `tags: [ai, skill]` from main's extension.yaml scaffold.
- Drop main's scaffold context.go/metadata.go; PR wires metadata via `azdext.NewMetadataCommand` in root.go and has no `context` subcommand.
- Bump `github.com/azure/azure-dev/cli/azd` to v1.25.0 to match main and re-tidy go.sum.
@huimiu huimiu force-pushed the huimiu/hui-add-skill-command branch from 2357b7b to 1b7af85 Compare May 19, 2026 06:58
huimiu added 3 commits May 19, 2026 15:17
Restore the original context.go and metadata.go files that were deleted, to respect the existing template structure. Rename skill_context.go to skill_client.go to avoid naming overlap with the restored context command.
…ght, exterrors

Add unit tests for previously uncovered pure-logic helpers:

- cmd/skill_download_test.go: archiveExtension format mapping, classifyExtractError sentinel-to-LocalError translation (unsafe/limit/collision/invalid + pass-through), and downloadAction.Run name validation short-circuit.
- cmd/skill_delete_test.go: deleteAction.Run rejects invalid names early and surfaces CodeMissingForceFlag when --no-prompt is set without --force.
- cmd/skill_validate_test.go: extend TestIsNotFound to cover real *azcore.ResponseError (404 vs 500, wrapped).
- exterrors/errors_test.go: validation/dependency/auth factories and ServiceFromAzure (wraps *azcore.ResponseError, pass-through, nil); package now at 100% coverage.
go fix -diff was reporting two conflicting suggestions on the same loop (slicescontains and stringsseq), causing the analyzer to exit non-zero. With bash -e in the lint-go workflow that non-zero exit short-circuited the script, breaking CI even though there was no real diff to print. Replacing the manual loop with slices.Contains resolves the conflict so go fix -diff exits 0.
)
}

body, err := skillCtx.client.Download(ctx, a.flags.name)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's expected to be able to download a single skill - it should materialize the skill file.

Copy link
Copy Markdown
Contributor

@wbreza wbreza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the scope and care on this PR, @huimiu — there's a lot of solid work here: thorough archive safety guards, well-structured exterrors, good test patterns where they exist, and clean alignment with the azure.ai.agents extension. The notes below are intended to be additive to the existing feedback from @trangevi, @therealjohn, @jongio, and Copilot — I tried hard not to duplicate anything they already raised. Where I've flagged concerns, I've tried to explain the why and point at references so the reasoning is portable to similar code later.

🔴 HIGH — Two functional bugs worth fixing before merge

1. update silently drops the Instructions field

internal/cmd/skill_update.go — in the GET-merge-POST flow, UpdateRequest is initialized with Description and Metadata from current, but Instructions is not carried forward. If a user runs azd ai skill update <name> --description "new" without providing --instructions, the merge writes back an empty Instructions field, silently wiping the existing body.

req := skill_api.UpdateRequest{
    Description:  current.Description,
    Metadata:     current.Metadata,
    // Instructions: current.Instructions,  // <-- missing
}

This is the classic GET-modify-PUT trap when a partial-update API is layered on top of a full-replace endpoint. Worth adding a regression test in a new skill_update_test.go that exercises "update only description, verify instructions preserved." See AGENTS.md → Error Handling / partial updates for the established pattern.

2. PeekArchiveSkillName swallows errors → create --force safety guard can be bypassed

internal/pkg/skill_api/archive_peek.go — the function returns ("", nil) on three error paths (entry.Open(), io.ReadAll(), ParseSkillMd()). The create --force flow relies on this name-match check to confirm "the archive I'm about to delete-then-recreate actually claims to be the skill the user named." A malformed archive currently returns "no name claim" (✓ from the caller's POV), so --force proceeds and deletes the existing skill anyway.

There are two reasonable fixes:

  • Strict (safer default): propagate the error so the caller refuses the destructive op until the archive is readable.
  • Lenient with telemetry: log a clear warning so the user sees that the safety check was skipped, and require an extra flag (e.g. --allow-unverified) before proceeding.

Either way, please don't let an unreadable archive look identical to "intentionally unnamed archive" — those should be distinguishable outcomes.

🟠 MEDIUM — Security & resource hardening on the extraction path

(All below are new findings — not duplicates of Copilot's symlink/copyFile or verifyPackageNameMatches OOM comments.)

  • UNC path in zip-slip guardarchive.go ValidateEntryName: after normalizing backslashes to forward slashes, the function checks for Windows drive letters (C:) but not UNC roots (\\server\share//server/share). On Windows this can extract outside OutputDir. Suggest adding a strings.HasPrefix(slashed, "//") rejection after the drive-letter check, plus a test alongside the existing C:\Windows\Temp case.
  • Tar entry-count off-by-onearchive.go stageFromTarGz: entryCount++ then if entryCount > maxEntries accepts maxEntries + 1 entries before rejecting. Zip side uses len(zr.File) > maxEntries on the full slice (correct). Change tar to >= or move increment after the check for symmetry.
  • File-handle leak in PeekArchiveSkillNamearchive_peek.go: when io.ReadAll or ParseSkillMd fails, the function returns without closing the entry reader. Pair the open with defer rc.Close() immediately after a successful entry.Open().
  • Tar hdr.Linkname not asserted emptyarchive.go stageFromTarGz: the default: branch in the typeflag switch catches TypeSymlink/TypeLink/etc., which is correct, but an explicit if hdr.Linkname != "" { return error } for the regular-file branch makes the intent obvious and protects against future tar variants that smuggle link intent in extension headers (PAX/GNU).
  • Partial output dir not cleaned on extraction failureskill_download.go: when SafeExtract fails mid-stream, the user-visible OutputDir may contain partially-extracted files. Either (a) extract into a sibling temp dir and atomic-rename on success, or (b) os.RemoveAll(opts.OutputDir) on error when the directory was created by this invocation. This also helps with --force re-runs.

🟠 MEDIUM — Test coverage on the two largest command files

skill_create.go (~409 LOC, 3 mutually-exclusive modes + --force delete-then-create sequence + name-match guard) and skill_update.go (~181 LOC, GET-merge-POST + flag validation + gzip rejection) currently have no _test.go companions. Given how much of the user-facing surface area lives in these two files — and that bug #1 above sits squarely inside one of them — these are the highest-leverage tests to add. The existing archive_test.go is a great template for the table-driven style.

Suggested table-driven cases at minimum:

  • skill_create_test.go: each mode (inline / .md / .zip), conflicting flag combos, --force with name match, --force with name mismatch, --no-prompt without required flags.
  • skill_update_test.go: merge preserves all current fields when only one is changed, .zip rejection error message, conflicting flags.

🟡 MEDIUM — PR description vs. implementation alignment

The PR description and CHANGELOG say create accepts a "gzip package", but selectCreateMode() only matches .zip, and upload Content-Type is hardcoded to application/zip. Downloads correctly auto-detect both formats. Either:

  • update the PR description / CHANGELOG / README to say "ZIP package on upload; ZIP or gzip-tar on download", or
  • extend selectCreateMode() to accept .tar.gz / .tgz and set the right content type.

This pairs with @copilot's earlier note on models.go package doc and README --file ./skill.zip.

🟢 LOW / NIT — Suggestions, not blockers

  • Cobra MarkFlagsMutuallyExclusiveskill_create.go hand-rolls the inlineProvided && fileProvided check. Cobra's built-in is more durable as flags accrete: cmd.MarkFlagsMutuallyExclusive("description", "file") (and similar for instructions/file). Worth a tiny follow-up.
  • --force semantics diverge across commandscreate --force = destructive replace, delete --force = skip confirm, download --force = overwrite. All three are reasonable in isolation, but consider whether create --force deserves a more explicit name (--replace / --recreate) given that it deletes user data. At minimum, the create flag's help text could lead with "destroys the existing skill."
  • update error message — when rejecting .zip on update, the message points at create --force but doesn't mention that the path is destructive (skills aren't versioned). Adding "(this deletes the existing skill first)" to the suggestion would close the loop with @trangevi's earlier comment about destructive ops.
  • Endpoint cascade as shared helper — the 5-level cascade (flag → env → ai-skills config → ai-agents config → host env) is going to want to live in cli/azd/pkg/azdext/ the moment a third AI extension shows up. Not for this PR, but worth filing a follow-up so it doesn't get copy-pasted.
  • exterrors duplication with azure.ai.agentsValidation / Dependency / Auth / ServiceFromAzure factories are near-identical across the two extensions. Same recommendation as above: candidate for cli/azd/pkg/azdext/exterrors in a follow-up so a future error-handling tweak doesn't need parallel PRs.

Net: two real correctness bugs (#1 and #2) and a couple of Windows-path / resource-cleanup hardening items are the things I'd most like to see land before this merges. Everything else is polish or follow-up. Really nice extension overall — happy to dig into any of these if helpful.

Co-authored-by: wbreza <6540159+wbreza@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 19, 2026

Thanks for the scope and care on this PR, @huimiu — there's a lot of solid work here: thorough archive safety guards, well-structured exterrors, good test patterns where they exist, and clean alig...

Addressed in the latest commit:

HIGH bugs fixed:

  1. Instructions now carried forward in the GET-merge-POST path (req.Instructions = current.Instructions). Also added Instructions to Skill/skillWire so the GET response populates it.
  2. PeekArchiveSkillName now propagates errors from entry.Open(), io.ReadAll(), and ParseSkillMd() instead of returning ("", nil) — malformed archives are distinguishable from "no SKILL.md present". Also added defer rc.Close() to fix the file-handle leak.

Security hardening:

  • UNC path guard: validateEntryName now rejects //server/share (after backslash normalization).
  • Tar entry-count: restructured to check >= maxEntries before incrementing, symmetric with the ZIP upfront check.
  • hdr.Linkname assertion: regular-file tar entries with a non-empty Linkname are now rejected with ErrUnsafeEntry.
  • ZIP rejection error for update now includes "this deletes the existing skill first; skills are not versioned" in the suggestion.

Tests added:

  • skill_create_test.go: createAction early-exit validation paths + four verifyPackageNameMatches cases (match, mismatch, no SKILL.md, malformed SKILL.md).
  • skill_update_test.go: invalid name rejection, clean endpoint-resolution failure path, ZIP suggestion text asserting "deletes".
  • archive_test.go: UNC path cases + tar entry with Linkname.
  • archive_peek_test.go: malformed YAML now expects an error.

Items I've deferred as follow-ups (as suggested): partial-OutputDir cleanup on failure, endpoint cascade extraction to azdext, and exterrors deduplication with azure.ai.agents.

Please add tests where possible

Added tests in the latest commit:

  • skill_create_test.go: validates createAction early-exit paths (invalid name, --no-prompt with no input, conflicting flags) and verifyPackageNameMatches (name match, mismatch, no SKILL.md, malformed SKILL.md now propagates error).
  • skill_update_test.go: invalid name rejection, clean endpoint-resolution failure path, ZIP suggestion text.
  • archive_test.go and archive_peek_test.go: additional cases for UNC paths, tar Linkname, and malformed YAML error propagation.

Copilot finished work on behalf of wbreza May 19, 2026 17:22
Copilot AI requested a review from wbreza May 19, 2026 17:22
Copy link
Copy Markdown
Contributor

@wbreza wbreza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great turnaround, @huimiu — verified ad851b7d against my prior findings. All HIGH issues fixed correctly and most MEDIUMs landed cleanly. Approving from my side.

Verification

Prior finding Status
🔴 update drops Instructions field current.Instructions now carried forward; skill_update_test.go covers the preservation case
🔴 PeekArchiveSkillName swallows errors → --force bypass ✅ Errors propagate; TestVerifyPackageNameMatches_MalformedSkillMd confirms --force is blocked when the archive is unreadable
🟠 UNC path bypass in zip-slip guard ✅ Explicit // prefix rejection added; tests for both //server/share and \\server\share
🟠 Tar entry-count off-by-one ✅ Check before increment; now symmetric with the zip path
🟠 File-handle leak in PeekArchiveSkillName defer rc.Close() immediately after open
🟠 Tar hdr.Linkname not asserted empty ✅ Explicit check on the regular-file branch + TestSafeExtract_TarGzRejectsEntryWithLinkname
🟠 Missing tests for create/update ✅ Both skill_create_test.go and skill_update_test.go added — table-driven, cover conflicting flags, name mismatch under --force, and the field-preservation case
🟡 Gzip-vs-ZIP doc inconsistency models.go package doc and archive_peek.go now state the ZIP-upload / ZIP-or-gzip-download asymmetry explicitly
@therealjohn's --top / --orderby removal ✅ Both flags gone from skill_list.go

One deferred item (non-blocking)

The "partial OutputDir not cleaned on extraction failure" MEDIUM in skill_download.go isn't part of this commit. Fine to defer — the staging directory is cleaned up, and the user can re-run with --force to recover. Worth a follow-up issue when you have the bandwidth; an atomic temp-dir-then-rename on the publish phase would close it cleanly.

No new issues introduced

Skimmed all 9 changed files. No regressions, no over-corrections, no broken callers. Error propagation reads correctly throughout.

LGTM. ✅

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be named "skill_context.go"?

Comment thread cli/azd/extensions/azure.ai.skills/internal/cmd/skill_download.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add azd ai skill create/update/show/list/download/delete to manage Foundry Skills from any directory

7 participants