Add reserved global flags registry for extensions#7312
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a centralized registry of global “reserved” flags so extensions can be prevented from reusing azd-owned flags (avoiding collisions like -e).
Changes:
- Introduces
ReservedFlag+ canonicalReservedFlagslist and lookup helpers for short/long flags. - Builds short/long lookup indexes and exposes helper functions (
IsReserved*,GetReserved*). - Adds a focused test suite covering lookup behavior, duplicates, and registry consistency.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| cli/azd/internal/reserved_flags.go | Adds the reserved flags registry, lookup maps, and helper APIs. |
| cli/azd/internal/reserved_flags_test.go | Adds unit tests validating registry population, lookups, and invariants. |
spboyer
left a comment
There was a problem hiding this comment.
Code Review
1. Universal reserved-flag check will break existing extensions
cli/azd/pkg/azdext/reserved_flags.go:22 — High
The registry treats environment, output, docs, etc. as universally reserved for every extension. azdext.Run() exits on any match before command execution. This is broader than what azd actually pre-parses today (CreateGlobalFlagSet only pre-parses cwd, debug, no-prompt, trace-log-file, trace-log-url). In-tree extensions like azure.ai.agents (uses -e for --environment, -o for --output) and azure.ai.finetune (uses -e for --project-endpoint) will fail at startup.
Fix: Only reserve flags that are truly universal, or scope the extra reservations to roots created with NewExtensionRootCommand() that actually register those globals.
2. collectConflicts skip logic masks subcommand flag conflicts
cli/azd/pkg/azdext/reserved_flags.go (collectConflicts function) — Medium
The skip logic uses name-based lookup (root.PersistentFlags().Lookup(f.Name)) to avoid flagging the SDK's own persistent flags. But before Cobra's mergePersistentFlags (which happens during Execute(), not validation), a subcommand's cmd.Flags().VisitAll() only yields locally-defined flags. If a subcommand locally defines a flag with the same long name as a root persistent flag (e.g., --output meaning "output file" instead of "output format"), the name lookup finds root's flag and skips validation entirely. The conflict goes unreported.
Fix: Use pointer equality instead of name equality:
if rootFlag := root.PersistentFlags().Lookup(f.Name); rootFlag != nil && rootFlag == f {
return
}517a3cc to
191e9bb
Compare
|
Responding to @spboyer's review: 1. Universal reserved-flag check will break existing extensions All 9 flags in the reserved list are legitimate azd globals: The extensions that currently collide ( 2. collectConflicts skip logic masks subcommand flag conflicts Good catch. Implemented pointer equality in d14bf4b — now only skips when the flag is literally the same |
d14bf4b to
5288d7b
Compare
VSCode Extension Installation Instructions
|
Observation: Two extension authoring models and the enforcement gapAfter PR #6856 introduced
The reserved flag enforcement in this PR only applies to extensions that opt into This creates a gap that extension authors need to understand when choosing how to build their extension. I've created:
Worth considering whether the design doc in this PR ( |
|
Great analysis. You're right — enforcement only covers SDK-managed extensions using Since all in-repo extensions are Go, the simplest path is to have each self-managed extension add a Added a 'Scope & Limitations' section to the design doc in this PR referencing #7405 and your authoring models doc in #7406. The migration can happen incrementally as a follow-up. |
|
/azp run azure-dev - cli |
|
Azure Pipelines successfully started running 1 pipeline(s). |
Introduce a formal registry of azd's global flags that extensions must not reuse. This prevents flag collisions like the -e conflict that caused #7271 where an extension's -e (--project-endpoint) shadowed azd's -e (--environment). The registry lists all global flags (-e, -C, -o, -h, --debug, --no-prompt, --docs, --trace-log-file, --trace-log-url) with lookup helpers that can be used for build-time validation and runtime conflict detection. Closes #7307 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ValidateNoReservedFlagConflicts() walks the extension command tree before execution and errors if any extension-defined flag collides with a reserved azd global flag (e.g. -e, -C, -o). This catches conflicts at extension development/test time, regardless of which repo the extension lives in. The SDK's own root persistent flags (--environment, --debug, etc.) are excluded from the check since they intentionally mirror azd globals. A sync test ensures the SDK and internal reserved flag lists stay aligned. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lags section - Add docs/specs/extension-flags/spec.md: core architecture spec documenting how azd pre-parses global flags, propagates via env vars, and dispatches to extensions. Includes reserved flag registry and Adding a New Global Flag checklist. - Update cli/azd/docs/extensions/extensions-style-guide.md: add Reserved Global Flags section under Parameter and Flag Consistency with flag table, DO/DON'T guidance, collision error example, and link to core spec. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move spec from docs/specs/extension-flags/spec.md to cli/azd/docs/design/extension-flag-architecture.md to follow the existing convention for azd design docs. Update cross-reference in extensions style guide. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Change ReservedFlags (exported slice) to reservedFlags (unexported) - Add ReservedFlags() function returning a defensive copy - Replace init() with func-literal var initializers to avoid ordering hazards - Fix 'Open Telemetry' → 'OpenTelemetry-compatible' spelling - Update test files and docs to use ReservedFlags() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tentFlags check - Fix spec test name reference (TestReservedFlagsInSyncWithInternal) - Categorize host-side flags by registration mechanism (CreateGlobalFlagSet vs root persistent vs cobra built-in) - Add short-name parity assertion to sync test - Check cmd.PersistentFlags() in collectConflicts (not just cmd.Flags()) - Add PersistentFlagCollision test case Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ag shadows The previous name-based lookup would skip validation when a subcommand defined its own flag with the same name as a root persistent flag. Using pointer equality ensures only the actual inherited root flag objects are skipped, while same-named flags on subcommands are still validated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ct compound conflicts, consistency fixes
The root persistent flag skip logic previously exempted ALL flags on root.PersistentFlags() via pointer equality. This meant an extension using a plain root (not NewExtensionRootCommand) could add a root persistent flag colliding with reserved globals without detection. Now only roots created by NewExtensionRootCommand (marked with an azd-sdk-root annotation) get the exemption. Extensions using plain roots have all their root persistent flags validated against the reserved list. Adds test proving plain-root collisions are caught. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document that reserved flag enforcement only applies to SDK-managed extensions using azdext.Run(). Self-managed extensions (6 of 9 in-repo) bypass enforcement. Reference #7405 and #7406 for tracking and the authoring models documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes accidental changes to detect_confirm_apphost.go that don't belong in this PR. Restores warning message display and removes magenta coloring that was part of a different feature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
8750eb3 to
9f96cf5
Compare
wbreza
left a comment
There was a problem hiding this comment.
Re-Review — PR #7312 (3 new commits since Mar 31 review)
Prior Findings: All 4 ✅ Resolved
| # | Priority | Finding | Status |
|---|---|---|---|
| 1 | High | Root persistent flag exemption allows reserved flag collisions | ✅ Fixed — azd-sdk-root annotation check gates the exemption |
| 2 | Medium | checkFlag short-circuits on first collision |
✅ Fixed — returns []FlagConflict for compound conflicts |
| 3 | Low | init() vs func-literal inconsistency |
✅ Fixed — func-literal var initializers |
| 4 | Low | commandPath O(n²) prepend-based slice building |
✅ Fixed — append + slices.Reverse |
New Commits Reviewed
| Commit | Assessment |
|---|---|
6011f2b fix: tighten root persistent flag exemption to SDK-created roots only |
✅ Correct — adds annotation guard, new test validates |
1dbc24f docs: add scope & limitations section for enforcement gap |
✅ Good — documents SDK-only enforcement boundary |
9f96cf5 fix: revert unrelated apphost changes |
✅ Clean revert |
New Findings: None
All 11 test cases pass. Build succeeds. Backwards compatibility preserved.
Overall: ✅ APPROVE — all findings resolved, SDK annotation approach is clean and correct.
Review performed with GitHub Copilot CLI
Azure Dev CLI Install InstructionsInstall scriptsMacOS/Linux
bash: pwsh: WindowsPowerShell install MSI install Standalone Binary
MSI
Documentationlearn.microsoft.com documentationtitle: Azure Developer CLI reference
|
| } | ||
|
|
||
| // reservedGlobalFlags is the canonical list of global flags that extensions must not reuse. | ||
| // Keep this in sync with internal.ReservedFlags and CreateGlobalFlagSet (auto_install.go). |
There was a problem hiding this comment.
Could we add a reference comment in auto_install.go as well?
| // reservedFlags is the canonical list of global flags that extensions must not reuse. | ||
| // It is derived from CreateGlobalFlagSet (auto_install.go), the root command's | ||
| // persistent flags, and the extension SDK's built-in flag set (extension_command.go). | ||
| // | ||
| // Keep this list in sync whenever a new global flag is added to azd. | ||
| var reservedFlags = []ReservedFlag{ |
There was a problem hiding this comment.
I'm slightly concerned with the duplication of global flags across 3 different files. Is it worth centralizing it today? I believe we're blocked waiting on the extension updates to occur and we may have more time to clean this up further.
| {Long: "trace-log-file", Short: "", Description: "Write a diagnostics trace to a file."}, | ||
| {Long: "trace-log-url", Short: "", Description: "Send traces to an OpenTelemetry-compatible endpoint."}, |
There was a problem hiding this comment.
These feel like internals that azd should handle through the SDK rather than via the CLI interface.
| {Long: "cwd", Short: "C", Description: "Sets the current working directory."}, | ||
| {Long: "debug", Short: "", Description: "Enables debugging and diagnostics logging."}, | ||
| {Long: "no-prompt", Short: "", Description: "Accepts the default value instead of prompting."}, | ||
| {Long: "output", Short: "o", Description: "The output format (json, table, none)."}, |
There was a problem hiding this comment.
The output and environment flag are NOT global in azd. They are lit up per-command based on the intended usage. How should one reconcile the differences?
Summary
Adds a formal registry of reserved global short flags that extensions must not reuse. This prevents future conflicts like the
-ecollision that caused #7271.What's included
cli/azd/internal/reserved_flags.go-ReservedFlagstruct with 9 entries (-e,-C,-o,--debug,--no-prompt, etc.), lookup maps, and helper functions (IsReserved,GetReserved)cli/azd/internal/reserved_flags_test.go- 7 test functions with 29 subtests covering all lookup paths and edge casesWhy
@vhvb1989 raised this concern on #7035 - there should be a way to block extensions from using known global flags. This is the foundation for that enforcement.
Closes #7307
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com