Repository navigation
Conversation
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.
|
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:
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.
There was a problem hiding this comment.
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
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-clientand 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.
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.
HarshCasper
left a comment
There was a problem hiding this comment.
I read the whole diff rather than sampling it, so this first round is about scope only. The reason #79 was reverted still applies here: at +77,071 / -264 across 224 files I can't review this change in a way that means anything. For comparison, the AWS client landed in about 330 lines (#3) and the Snowflake client in 235 (#21), and all of src/ on main is 14,695 lines. The tool itself is a good idea and parts of the implementation are careful. The problem is everything built around it.
What I'm asking for in this round: cut the PR down, on this same branch, to the tool, the service: "azure" change in localstack-management, and their unit tests. Under 3,000 lines including tests is the target. Put everything that comes out on a separate branch or a draft PR so it isn't lost; most of it can come back later in smaller PRs, each with the evidence it needs. Round 2 will be an ordinary correctness review of what remains.
What stays
Most of this already exists in the PR and only needs trimming.
src/tools/localstack-azure-client.tswithout theversionspecial case, the test envelope andrequireStack.bootstrap.tskeepsCLI_CONFIG,DUMMY_LOGIN,cloudConfigJson, the fiveazcalls and the marker file. The lock directory, the lease files, the version-check seed and the self-heal can go.runner.tsandchild-env.tskeep the spawn core (it fixes real problems incommand-runner.ts) and the allow-list environment. The semaphore, the Windows command-line length pre-check and the kill-step hooks can go.resolve-az.tskeepsLOCALSTACK_AZ_PATH, a PATH walk, theaz.cmdtopython.exemapping and the version probe. The launcher-text parsing per installer, the WSL guard and the localaz versionanswer can go.policy.tskeeps the shared tokenizer, an exact-match deny list of command prefixes and flags (login, logout, account clear, cloud, config, extension, upgrade, interactive, find, feedback, the browser and tunnel commands), themanagement.azure.comrewrite, and the file rule for existing files under the workdir. The two generated JSON tables,scripts/gen-az-file-args.pyandleak-commands.jsoncan go.emulator.tsandruntime-status.tsas they are, minus the guard-off branch.output.tscut down to: stdout on exit 0; otherwise stderr with the traceback frames stripped, a size cap, and the not-implemented and missing-extension hints. About 80 lines instead of 794 plus 1,100 lines of fixtures.- The tool's configuration moved from
src/core/config.tsintosrc/lib/azure, with these settings only:LOCALSTACK_AZURE_PORT,LOCALSTACK_AZURE_ENDPOINT,LOCALSTACK_AZURE_IMAGE_NAME,LOCALSTACK_AZ_PATH,LOCALSTACK_AZ_CONFIG_DIR,LOCALSTACK_AZ_WORKDIR,LOCALSTACK_AZ_TIMEOUT_SECONDS. - In
localstack-management: the image name, forwarding ofLS_AZURE_*, the volume note. - Unit tests for the above, trimmed with the code, plus
tests/mcp/azure-offline.spec.mjs. - README: a short setup section and rows for the seven settings.
What comes out, and why
-
Tests of the emulator,
azand the samples repo.tests/azure/matrix(393 cases over 29 provider files, 61 datedknown_gapentries, an operation catalogue the README says is for the portal), the drift gates, the samples shim and replay, the 16,831-line corpus, the egress-internal experiment, and the E2 harness intests/azure/evals(11,888 lines; a Claude agent loop with a spend cap). All of these measure the emulator,az, the samples repo or model behaviour. The PR description makes the point that new emulator coverage reaches users without a code change here; DR6 then requires a new YAML file in this repo for every provider the emulator adds, and the weekly job opens an issue when one is missing. That dependency points the wrong way. A per-service matrix, if the team wants one, belongs in the emulator repo and should driveazdirectly. What this repo needs is one short live smoke, around ten commands through the tool, onworkflow_dispatchand a weekly schedule. -
Snapshots of
azinternals that only the weekly job keeps honest.az-command-words.generated.jsonandaz-file-args.generated.json(2,926 lines generated fromazure-cliinternals), 26 extension pins taken against 2.90.0, the Bicep version and sha256 repeated in the Dockerfile,ci.yml, the composite action,docker.ymlandbicep-install.ts, 43 stderr fixtures recorded on Windows from oneazversion, and about 35 regexes overazstderr wording behind 25 failure classes inoutput.ts. The AWS client has two regexes. Every one of these goes stale as soon asazchanges, and nothing in the normal CI run catches it. -
Changes to code every AWS user runs.
src/core/preflight.tsandsrc/core/config.tsnow import fromsrc/lib/azure, and every tool imports preflight, so each tool's lazy chunk carries the Azure library. I built both commits: per-tool chunks go from about 190 KB to 320 KB and the unpacked package from 3.4 MB to 5.6 MB. The Azure preflights and config need to live undersrc/lib/azure.requireStackin twelve tools. For the Azure tool it repeats the edition checkemulator.tsalready does, through a second HTTP client. For the AWS tools I couldn't construct a case where it helps; it was behind one of the HIGH findings on #79; and with an AWS container running it now refuseslocalstack-snowflake-client, where main would at least runsnow. Take it out of this PR. If there's a case for it, it can be a PR of its own.- The container lookup changes (stack filtering, the published-port rule) and the side-by-side mode (
LOCALSTACK_AZURE_PORT, a restart that carries an external container's env, the unmountable-bind check). Side-by-side also has a loop today: withLOCALSTACK_AZURE_PORTset and no Azure emulator,statusand the tool both say to runstart, andstartrefuses with "Conflicting Azure port settings". Each of these can come back on its own with a scenario that fails without it.
-
CI and process.
ci.ymlmoves every PR to a three-OS matrix with a 90% coverage gate.azure-live.ymlstarts a real emulator on each PR (11 minutes, plus 16 for the samples job on this run).azure-weekly.ymlhas eight jobs and files issues assigned to me.CODEOWNERSadds the team and me to every Azure path. And the push job indocker.ymlnow depends on the oneLOCALSTACK_AUTH_TOKENbeing entitled for Azure, so no image publishes when it isn't. For this PR:ci.ymlstays as on main, one dispatch-plus-weekly smoke job replaces the two workflows, noCODEOWNERS, no auto-filed issues. -
The Docker image goes from 232 MB to 418 MB compressed, 110 MB of it the Bicep binary, for every user of the image. Whether to bundle
azand Bicep at all is a separate decision. Take it out of this PR together with the loopback forwarder (306 lines), which exists only because the health checks dial 127.0.0.1; inside the image the upstream host can beLOCALSTACK_HOSTNAME, as for the other tools. -
The warm worker:
worker-runner.ts,worker-script.tsand the fake-worker fixture, 1,074 lines with Python kept inside a TypeScript string. It is off by default, no CI job runs it against a realaz, and the README's "about 5x faster" has no benchmark in the repo. It can come back with one. -
install-azure-addonsand what sits behind it:extension-install.ts,bicep-install.ts, the wizard steps,scripts/install-azure-extensions.mjs, about 2,200 lines in all. The tool creates its own need for this:child-env.tspointsAZURE_EXTENSION_DIRat a private directory and the bootstrap setsextension.use_dynamic_install=no, so whatever the user has installed withaz extension addis invisible. Let the child inherit the user's extension directory read-only, keep the hint map inextension-map.ts(about 95 lines), and document Bicep with brew, winget oraz bicep install. -
The egress guard,
egress-proxy.ts(676 lines plus 953 of tests). The profile holds a dummy login and every cloud endpoint points at the emulator, so a request that did reach real Azure would carry no credential. A guard can still come in later, as its own PR, in the CONNECT-only form (around 180 lines) and with the threat model written down.
Smaller items that leave with the above: LOCALSTACK_AZ_DENYLIST_FILE, _MAX_HELP_CHARS, _RUNNER, _PYCACHE_DIR, _TEST_ENVELOPE, _FORWARD_TARGET, _EGRESS_GUARD, _BICEP_ENV, _EXTENSION_DIR and _BICEP_PATH, which takes server.json from 37 settings to about 27; the test-only envelope in the handler, which only the live suites read; and the tool description, which is 1,634 characters (the next largest tool's is 261) and embeds the server's absolute working directory. Three or four static sentences are enough there.
After the cut
Round 2 is a normal review of what's left. The rest can follow as separate PRs in whatever order suits the team: the egress guard with its threat model, the extension and Bicep handling, image bundling, the lifecycle extras, requireStack, and the worker with a benchmark. Each should be small enough to review in one sitting. If anything in this list is unclear, ask here and I'll expand on it.
Thanks @HarshCasper for the thorough review, I'm happy to cut the PR down to the scope you described but there are 4 points I'd like to settle first:
Everything that comes out goes to a parked branch with a draft PR, so it can come back in smaller PRs with evidence. |
|
Thanks, all four are fair questions. Here is where I land on each.
The parked branch with a draft PR is the right way to keep the rest. Go ahead with the cut. |
|
Thanks @HarshCasper, the cut is pushed. This PR is now the Azure tool, Per your review
Small things I kept, so you can push back on them
These two and their tests are why the total sits about 40 lines over 3,000 once the Parked: the full version is in draft #88, not for merge. Still to do after this merges
|

What
Adds
localstack-azure-client, which runs Azure CLI (az) commands against the LocalStack for Azure emulator, andservice: "azure"inlocalstack-management(start, stop, restart and status).az(2.85 or newer) with a profile of its own (LOCALSTACK_AZ_CONFIG_DIR), logged in to the emulator with a dummy account. The user's Azure login is never used or changed...path traversal is refused, as in the AWS client. Amanagement.azure.comURL given torestbecomes a relative path.azis stopped on a timeout, on a client cancel, and when the server itself stops. Servers sharing a profile set it up one at a time.LOCALSTACK_AZURE_ENDPOINT,LOCALSTACK_AZURE_IMAGE_NAME,LOCALSTACK_AZ_PATH,LOCALSTACK_AZ_CONFIG_DIR,LOCALSTACK_AZ_WORKDIRandLOCALSTACK_AZ_TIMEOUT_SECONDS. The tool reaches the emulator throughLOCALSTACK_HOSTNAMEandLOCALSTACK_PORT, as the other tools do.azyet; inside it, the tool says so.Tests
az, the child environment, the runner, the profile bootstrap, the emulator checks and the output, plus the tool itself andservice: azureinlocalstack-management.tests/mcp/azure-offline.spec.mjsruns inmcp-direct-tests, without an emulator..github/workflows/azure-smoke.yml(on demand and weekly) starts the emulator withlocalstack-managementand runs a short tour ofazcommands through the tool.Not in this PR
Each of these comes later as its own small PR, with steps that reproduce the case it handles. The earlier full version is parked in draft #88, not for merge, and the follow-ups take their code from there.
azfrom reaching hosts other than the emulator, with the threat model written down.az, Bicep and a loopback forwarder, so the Azure tool works inside the image.requireStack: a clear answer when a tool meets another stack's emulator (AWS, Snowflake or Azure).azcalls, with a benchmark script.