Skip to content

feat(azure): add the localstack-azure-client tool and Azure lifecycle support - #86

Open
DrisDary wants to merge 12 commits into
mainfrom
feat/azure-client
Open

DrisDary wants to merge 12 commits into
mainfrom
feat/azure-client

Conversation

@DrisDary

@DrisDary DrisDary commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

This server covers LocalStack's AWS and Snowflake emulators. This PR adds the LocalStack for
Azure emulator:

  • A new tool, localstack-azure-client, runs Azure CLI (az) commands against the LocalStack
    for Azure emulator, never real Azure. Whatever az can do and the emulator implements works,
    with az's own validation and --help; for an operation az has no command for, rest with a
    relative URL reaches the emulator.
  • localstack-management manages the Azure emulator with service: "azure": start, stop,
    restart and status.

Why the Azure CLI: az already covers every Azure API, so new emulator coverage reaches users
without a code change here, and one tool keeps the catalogue small.

Why the user's own az, the way the Snowflake tool uses snow: the Azure emulator image ships
no client CLI (no az, Bicep or azlocal), unlike the AWS image, whose awslocal the AWS tool runs
inside the container. So the tool runs the az on the user's machine, in an isolated profile, and
says how to install it when it is missing. The Docker image bundles az, the extensions and Bicep.

Changes

How it fits together

%%{init: {"theme":"base","flowchart":{"wrappingWidth":370},"themeVariables":{"fontFamily":"Segoe UI, Helvetica, Arial, sans-serif","fontSize":"14px","primaryColor":"#eef3fb","primaryTextColor":"#1a1a1a","primaryBorderColor":"#5d7aa8","lineColor":"#5f6b7a","secondaryColor":"#f4f6f8","tertiaryColor":"#ffffff","clusterBkg":"#f7f9fc","clusterBorder":"#9aa6b6","titleColor":"#1a1a1a","edgeLabelBackground":"#ffffff","textColor":"#1a1a1a"},"themeCSS":"background-color:#ffffff;"}}%%
flowchart TB
  subgraph canvas[" "]
    direction TB
    client["MCP client<br/>Claude Code, Cursor, VS Code, ..."]

    subgraph server["LocalStack MCP server (npx, or the Docker image)"]
      direction TB
      azure["localstack-azure-client<br/>one az command per call"]
      mgmt["localstack-management<br/>service: azure"]
      policy["Command policy<br/>refuses shell syntax, logins, config changes,<br/>extension installs, and browser, shell or tunnel commands;<br/>keeps files in the workdir; makes ARM URLs relative"]
      runner["Runner<br/>own az profile: ~/.localstack/azure/mcp-config-{port}<br/>allow-list environment, private home and temp"]
      guard["Egress guard (in-process proxy)<br/>allows only the emulator's host names and ports"]
      output["Answer<br/>az's output, or its error with a class and a hint"]
    end

    subgraph host["Same host (bundled in the Docker image)"]
      az["Azure CLI (az 2.85+)<br/>26 pinned extensions and Bicep<br/>installed by install-azure-addons"]
    end

    subgraph docker["Docker"]
      emulator["LocalStack for Azure emulator<br/>localhost.localstack.cloud:4566"]
    end

    real["Real Azure<br/>management.azure.com"]
  end

  client -- "MCP (stdio)" --> server
  azure --> policy --> runner
  runner -- "spawns" --> az
  az -- "HTTPS_PROXY" --> guard
  guard -- "allowed" --> emulator
  guard -- "refused" --x real
  runner --> output
  mgmt -- "Docker Engine API" --> emulator

  style canvas fill:#ffffff,stroke:#d0d7de,color:#1a1a1a
  classDef tool fill:#e7efff,stroke:#4a6fa5,color:#1a1a1a;
  classDef step fill:#ffffff,stroke:#8a96a8,color:#1a1a1a;
  classDef cli fill:#fff4dc,stroke:#b7862a,color:#1a1a1a;
  classDef emu fill:#e3f5e8,stroke:#3f8f5a,color:#1a1a1a;
  classDef blocked fill:#fde8e8,stroke:#c0504d,color:#1a1a1a;
  classDef muted fill:#f2f2f2,stroke:#a0a0a0,color:#1a1a1a;
  class azure,mgmt tool;
  class policy,runner,guard,output step;
  class az cli;
  class emulator emu;
  class real blocked;
  class client muted;
Loading

localstack-azure-client checks each command against its policy, then runs the Azure CLI on the
same host in the tool's own profile. Every connection az makes goes through the egress guard,
which lets it reach only the emulator. localstack-management starts, stops and restarts the
emulator's container through the Docker Engine API.

The Azure tool: localstack-azure-client

One input, command: an az command without the leading az.

  • Finding az (src/lib/azure/resolve-az.ts): MSI, pip, Homebrew and deb installs, 2.85 or
    newer. It spawns the CLI's own Python (-X utf8 -IBm azure.cli), never through a shell or a
    .cmd launcher. LOCALSTACK_AZ_PATH overrides the lookup.
  • An isolated CLI profile (bootstrap.ts): the tool's own AZURE_CONFIG_DIR
    (~/.localstack/azure/mcp-config-<port>) with a LocalStack cloud and a dummy login, plus a
    private home and temp folder. The user's own az login is never used or changed.
  • Containment (src/lib/azure/):
    • policy.ts refuses shell syntax, logins, cloud and config changes, extension installs,
      upgrade, and commands that open a browser, shell or tunnel or run Docker. File arguments
      must stay inside LOCALSTACK_AZ_WORKDIR, and never reach ~/.azure, ~/.ssh, ~/.kube or
      ~/.docker, checked as the OS will open them (through symlinks, junctions and 8.3 names).
    • child-env.ts gives az an allow-list environment: the user's AZURE_*, ARM_*, proxy and
      CA variables never reach it.
    • egress-proxy.ts routes every connection az or Bicep makes through a local proxy that allows
      only the emulator's host names and ports (its gateway, 443 and its service range), so other
      local services, such as Docker's API, stay out of reach. Absolute management.azure.com URLs
      given to rest are rewritten to relative ones.
  • Answers an agent can act on (output.ts): az's own error text, a failure class in the
    first line (not-implemented, extension, bicep-missing, egress-refused, …) and a hint.
  • Bicep: .bicep and .bicepparam deployments work (LOCALSTACK_AZ_BICEP_PATH, then the
    tool's own folder, then PATH).
  • A warm worker, experimental and off by default (LOCALSTACK_AZ_RUNNER=worker).

Setup, the Snowflake way

  • The init wizard is unchanged.
  • npx -y @localstack/localstack-mcp-server install-azure-addons installs the 26 pinned az
    extensions and Bicep 0.47.16 (sha256-checked) into the tool's own folders, never into
    ~/.azure (--no-extensions, --no-bicep).
  • A missing az answers with install commands for Windows, macOS and Debian/Ubuntu, as a missing
    snow does; a missing extension or Bicep names install-azure-addons.
  • README: "Setting up the Azure tool", the LOCALSTACK_AZ_* settings, and "How the Azure tool
    stays local".

Shared code (AWS and Snowflake run through it)

  • A stack check before every stack-specific tool (requireStack, src/core/preflight.ts): a
    tool that meets another stack's emulator refuses and names the right tool. The stack comes from
    the health edition, then the container's image and labels; unknown passes (fail-open). The
    AWS-only tools refuse on the Azure emulator.
  • localstack-management: service: "azure"; status names the container it found;
    restart refuses, before stopping anything, a container whose state folder this machine cannot
    mount; restart of an externally started container (lstk, docker run) keeps that
    container's own env flags, for AWS and Snowflake too, unless this server's env block sets
    them. A container this server started behaves as before. With the Azure emulator beside an AWS
    one (LOCALSTACK_AZURE_PORT set), status and stop act on the Azure container, and restart
    refuses before stopping anything. A start forwards the Azure emulator's own settings
    (LS_AZURE_*, MSSQL_ACCEPT_EULA, …) from the env block.
  • The container lookup knows the Azure image and lstk's names; with LOCALSTACK_PORT set it
    takes only a container that publishes that port, or the one known container that publishes none
    (host or compose networking). The AWS client also runs in the Snowflake emulator's container,
    which serves the AWS APIs.
  • The AWS CLI tokenizer moved to src/lib/cli/argv.ts, shared by both clients; the AWS client
    calls it without options, so its behaviour is unchanged.
  • No new runtime dependencies (@anthropic-ai/sdk and yaml are dev dependencies).

Docker image

az 2.90 in its own venv, the 26 pinned extensions and Bicep 0.47.16 (ADD --checksum).
Compressed: 418 MB, against 245 MB without them. tests/docker/image-size.mjs gates the growth.

Run from the image, the server keeps the emulator's state in a named volume by default. The Azure
emulator runs Function and Web Apps in containers of their own and shares their files from its
state folder, which it can do only from a host folder: to deploy them, set LOCALSTACK_VOLUME_DIR.
localstack-management start (service azure) says so when it starts the emulator on a named
volume.

CI

  • ci.yml: unit tests on Ubuntu, Windows and macOS with a 90 % line-coverage gate on the Azure
    code, type-checks of the test code, prettier on the changed files, and an az spawn smoke test.
  • docker.yml: image assertions (amd64 and arm64), the size gate, and the image against real
    emulators: AWS and Snowflake, then the Azure stage in a run of its own.
  • azure-live.yml (new; path-filtered PRs and main): a real LocalStack for Azure emulator per
    job, with its state folder bind-mounted (Function App deployments need it); a subset of the
    command matrix, the egress checks, the harness's Azure stage and three official samples.
  • Every job uses the one LOCALSTACK_AUTH_TOKEN secret, so its licence must cover AWS, Snowflake
    and Azure. An emulator that exits (such as on a licence failure) ends the job at once with its
    reason.
  • azure-weekly.yml (new; Mondays): the full matrix, egress on an internal network, all official
    samples, drift gates, and az 2.85 and the latest. No job calls a model API: the model evals
    run locally only.
  • A failed weekly run, or a failed azure-live.yml on main, opens or updates a tracking issue
    that mentions @localstack/smurf and @HarshCasper and is assigned to Harsh.
  • .github/CODEOWNERS (new): @localstack/smurf and @HarshCasper on the Azure-only paths.
  • .gitattributes (new): shell scripts and the matrix YAML stay LF, .cmd files CRLF.

After the first review

  • The file rule checks paths as the OS opens them: through symlinks, junctions and 8.3 names,
    for files that do not exist yet, and for a .. after a link.
  • Analytics keep only flag names, also when a value is stuck to a short option (-pSecret).
  • The egress guard and the URL rule allow only the emulator's ports.
  • The AWS client runs in the Snowflake emulator's container. With LOCALSTACK_PORT set, a
    container that publishes no port is still found.
  • Beside an AWS emulator, stop (service azure) acts on the Azure container, and restart
    refuses before stopping anything. On restart, this server's env block wins over the old
    container's flags. A start forwards the Azure emulator's LS_AZURE_* settings.
  • A venv's Python in LOCALSTACK_AZ_PATH runs by its own path. A Windows az behind any WSL mount
    root is refused.
  • The warm worker resets logging for each command and honours a cancel during its start.
  • In Docker, LOCALSTACK_HOSTNAME is tried for the emulator; the hints name LOCALSTACK_PORT.
  • install-azure-addons --no-extensions works without az; server.json lists
    LOCALSTACK_AZ_BICEP_ENV; the tool description no longer claims a default location.
  • CI: no job needs an Anthropic API key; the weekly workflow sets the auth token only on the steps
    that need it; the live path filter covers src/core, src/cli and the Azure fixtures.

Tests

  • Unit: 1,639 tests, none failing on Windows or Linux, also with the temp directory behind a
    link (as macOS's /var and Windows 8.3 paths are); line coverage of the Azure code 92.8 %
    (gate: 90 %).
  • Live, against a LocalStack for Azure emulator (tests/azure/README.md lists the layers):
    the command matrix (393 cases), the egress checks, the official samples through an az shim, the
    MCP harness through npx and the image, the image assertions, and drift gates. The PR subset runs
    in this PR's azure-live.yml; the rest weekly.
  • On this PR's CI: every job passes: live (the matrix subset, the egress checks and the
    harness through npx) and the samples subset against the emulator, docker.yml's smoke test
    (AWS, Snowflake and the Azure stage), and its L5 jobs on arm64 and against a 127.0.0.1-published
    emulator.

Related

Adds localstack-azure-client, a tool that runs the Azure CLI against the
LocalStack for Azure emulator (never real Azure), and extends
localstack-management to start, stop, restart and report the Azure
emulator.

- The tool runs the host's az (2.85 or newer) in an isolated CLI profile
  that is logged in to the emulator only. A policy refuses shell syntax,
  logins, profile changes, extension installs and commands that open a
  browser, shell or tunnel, and keeps file arguments inside the working
  directory and out of the user's credential folders. An egress guard
  rewrites absolute management.azure.com URLs and blocks every other
  host. Failures come back classified, with a hint. An experimental warm
  worker (LOCALSTACK_AZ_RUNNER=worker) keeps az imports loaded.
- localstack-management: the Azure stack's container spec and port
  checks. A restart carries an externally started container's own
  settings, and refuses a container this machine cannot recreate.
- Setup follows the Snowflake tool: the user installs the Azure CLI, and
  a missing az answers with the install commands. A new command,
  install-azure-addons, installs the pinned Azure CLI extensions and
  Bicep the tool uses. The init wizard is unchanged. The setup and the
  LOCALSTACK_AZ_* settings are documented in README.md and
  docs/DOCKER.md.
- The Docker image bundles az 2.90, the 26 pinned extensions and Bicep,
  with image assertions and a size gate.
- Tests: unit tests with an Azure coverage gate (90 % of lines), a live
  command matrix, the official Azure samples replayed through the tool,
  and model evals. New CI workflows: azure-live.yml and azure-weekly.yml.
- CODEOWNERS requests @localstack/smurf and @HarshCasper on the
  Azure-only paths. .gitattributes keeps shell scripts LF and .cmd
  files CRLF on every checkout.
Comments, docs and fixtures state facts without citing internal plans, reviews, checks, test runs or benchmarks. build-corpus.py now builds the samples corpus from the in-repo extract.py and bash_argv.py output (the same 736 cases), and scripts/extract-leak-commands.mjs is removed: its input was never in the repository. tests/azure/README.md defines the test layers the CI jobs name.
The README's Azure section keeps its notes on using the emulator with the other tools. The E2 evals describe their builders, variants and seven answer-reading pitfalls on their own terms, and recorded fixtures use neutral resource names.
…he token

Removes the weekly E2 job, the only one that needed an Anthropic API key. The emulator start stops as soon as the container exits and prints the licence reason. The ~/.azure fingerprint is taken before the emulator starts, the live path filter covers core, cli and the Azure fixtures, the weekly token is set per step, and the matrix YAML is pinned to LF.
File paths are checked as the OS opens them, analytics drop values stuck to short options, and the egress guard allows only the emulator's ports. The AWS client runs in the Snowflake container, stop and restart handle the Azure emulator beside an AWS one, and the MCP env block wins on restart. A venv Python in LOCALSTACK_AZ_PATH is run by its own path, and a Windows az behind any WSL mount is refused. Also fixes the warm worker's log level and cancel, LOCALSTACK_HOSTNAME and port hints, the tool description, server.json and fixture redaction, and removes internal references.
…cret

The Azure jobs and steps take their token from LOCALSTACK_AUTH_TOKEN_AZURE, else LOCALSTACK_AUTH_TOKEN. The smoke test runs its AWS and Snowflake stages with LOCALSTACK_AUTH_TOKEN and the Azure stage on its own with the Azure token. Also repairs four comments that an earlier cleanup left with stray punctuation.
… work

The Azure emulator shares files with the containers it starts for Function and Web Apps from /var/lib/localstack, and refuses to unless that folder is a bind mount. The CI start script and the internal-network job now bind-mount a runner folder there. Starting the Azure emulator on a named volume, as when this server runs in Docker, now says that app deployments need LOCALSTACK_VOLUME_DIR, and the Docker docs and README say so too.
One token whose licence covers AWS, Snowflake and Azure, with no fallback, so a gap in its licence fails the run instead of hiding behind LOCALSTACK_AUTH_TOKEN.
Its licence covers AWS, Snowflake and Azure, so the separate LOCALSTACK_AUTH_TOKEN_AZURE secret is gone.
@DrisDary
DrisDary requested review from a team, HarshCasper, paolosalvatori and remotesynth and removed request for HarshCasper October 5, 2026 12:37
@DrisDary DrisDary self-assigned this Oct 5, 2026
@DrisDary

DrisDary commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Why #86 fails now when the same code passed on 29 Sep:

Nothing in our code changed. #86 runs the same commit as #79's last green run (1f12bf3), and main after the revert is byte-for-byte what it was before #79. Two things outside the repo changed:

  1. A new high-severity advisory for @grpc/grpc-js (GHSA-m9gg-hp2v-232j) was published on 30 Sep, the day after the green run. Our lockfile pins 1.14.4 (pulled in by dockerode), so the audit-ci --high step now fails. main pins the same version, so every PR hits this. Fix: bump it to 1.14.5 in yarn.lock, as a small separate PR to main.

  2. The outside service behind the docs-search tool (CrawlChat) isn't answering today, so the two tests that call it time out after 15 seconds: the MCP Server Tester docs test and the Docker smoke docs scenario. Nothing to fix in our code; they'll pass again once CrawlChat is back.

Everything else passes, including all the Azure live tests and the AWS and Snowflake scenarios.

audit-ci --high now fails on this advisory, published on 30 Sep. It covers 1.14.0 to 1.14.4, which dockerode pulled in. Lockfile only.
localstack-docs calls an external search service. When that service times out, refuses, or returns a 5xx, the direct test is skipped and the image harness warns, each with the tool's answer. A 4xx or any other wrong answer still fails.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The review found a test parse failure, analytics data-exposure risk, and an unbounded warm-worker output path.

Review effort: Balanced
Findings: 2 High severity

Open (2)
What changed in this PR

Adds Azure emulator tooling, lifecycle management, containment, installation helpers, and extensive multi-layer validation.

Changes:

  • Adds localstack-azure-client and Azure-aware management/preflight behavior.
  • Adds isolated Azure CLI/Bicep execution with policy and egress controls.
  • Expands unit, live, drift, Docker, sample, and evaluation coverage.
File Description
src/​lib/​azure/​* Azure client runtime, policy, isolation, installation, and tests
src/​tools/​localstack-*.ts Stack-aware preflight checks for existing tools
src/​cli/​* Azure add-ons command and help
src/​lib/​wizard/​* Azure extension/Bicep setup helpers and tests
src/​lib/​cli/​argv.ts Shared CLI tokenizer
src/​lib/​aws/​aws-cli-sanitizer.ts Uses shared tokenizer
src/​core/​analytics.ts Azure analytics fields
src/​tools-tests/​localstack-management-ports.test.ts Port-safe lifecycle tests
tests/​azure/​** Live matrix, drift, samples, evals, and utilities
tests/​fixtures/​azure/​** Azure CLI, worker, corpus, Bicep, and drift fixtures
tests/​mcp/​** Azure MCP catalogue, offline, and Gemini tests
tests/​docker/​** Image contents and size gates
scripts/​ci/​** Azure CI startup, catalogue merge, and secret scanning
data/​sample-azure/​** Azure harness sample resources
data/​evals/​gemini-azure.json Azure tool-trigger dataset
.github/​workflows/​ci.yml Cross-platform tests, coverage, and spawn smoke tests
.github/​actions/​azure-live-setup/​action.yml Reusable Azure live-test setup
.github/​CODEOWNERS Azure path ownership
server.json Azure environment configuration
manifest.json Azure tool and prompt registration
package.json Azure test scripts and development dependencies
playwright.config.mjs Isolated offline Azure MCP project
jest.config.js Azure coverage and test TypeScript configuration
jest.azure-live.config.js Azure live-test projects
tsconfig.tests.json Test-code type checking
docker/​azure-extensions.txt Pinned Azure CLI extensions
docker/​image-size.json Recorded image-size baseline
.gitattributes Cross-platform line-ending rules
.gitignore Generated Azure test artifact exclusions
.prettierignore Generated Azure fixture exclusions

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/lib/azure/policy.test.ts
Comment thread src/lib/azure/policy.ts
command_path took every leading word-shaped token, so a positional value (az find <term>) or an unknown command could reach analytics. It now keeps a word only if az's command table has it. scripts/gen-az-file-args.py --command-words generates the list (az 2.90.0 with the 26 curated extensions: 7,048 commands, 1,592 words), and DR4 checks it on every pin move. The property test now also tries secrets passed as positional values.
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.

2 participants