feat(azure): add the localstack-azure-client tool and Azure lifecycle support - #79
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.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Stack detection, Bicep-only installation, worker output limits, fixture redaction, and empty-evaluation handling have unresolved correctness or security issues.
Review effort: Balanced
Findings: 2
Open (4)
What changed in this PR
Adds first-class LocalStack Azure support, including Azure CLI execution, emulator lifecycle management, containment controls, installation tooling, Docker packaging, and extensive test infrastructure.
Changes:
- Adds
localstack-azure-clientwith isolated profiles, egress controls, Bicep, extensions, and optional workers. - Extends lifecycle and stack detection across Azure, AWS, and Snowflake.
- Adds comprehensive unit, live, matrix, eval, drift, sample, and Docker-image coverage.
| File | Description |
|---|---|
.gitattributes |
Enforces script line endings. |
.github/CODEOWNERS |
Assigns Azure ownership. |
.github/actions/azure-live-setup/action.yml |
Sets up Azure CI dependencies. |
.github/workflows/azure-live.yml |
Runs PR Azure integration tests. |
.github/workflows/azure-weekly.yml |
Runs full weekly Azure tests. |
.github/workflows/ci.yml |
Expands cross-platform CI. |
.github/workflows/docker.yml |
Adds Azure image validation. |
.gitignore |
Ignores generated test artifacts. |
.prettierignore |
Excludes generated Azure fixtures. |
Dockerfile |
Bundles Azure CLI and Bicep. |
README.md |
Documents Azure setup and containment. |
data/evals/gemini-azure.json |
Defines Azure Gemini evaluations. |
data/sample-azure/hello.txt |
Adds harness sample data. |
data/sample-azure/main.bicep |
Adds sample Azure deployment. |
data/sample-azure/main.bicepparam |
Adds Bicep parameters. |
docker/azure-extensions.txt |
Pins Azure CLI extensions. |
docker/image-size.json |
Records image-size baseline. |
docs/DOCKER.md |
Documents Docker Azure behavior. |
jest.azure-live.config.js |
Configures live Azure suites. |
jest.config.js |
Adds test typing and exclusions. |
manifest.json |
Registers the Azure tool. |
package.json |
Adds scripts and test dependencies. |
playwright.config.mjs |
Adds offline Azure MCP tests. |
scripts/ci/azure-emulator-up.sh |
Starts CI Azure emulator. |
scripts/ci/merge-op-catalogue.cjs |
Merges matrix results. |
scripts/ci/scan-for-secret.mjs |
Scans artifacts for secrets. |
scripts/gen-az-file-args.py |
Generates Azure file-argument metadata. |
scripts/install-azure-extensions.mjs |
Installs pinned extensions. |
server.json |
Exposes Azure configuration. |
src/cli/azure-addons.test.ts |
Tests add-on installation. |
src/cli/azure-addons.ts |
Implements add-on command. |
src/cli/help.test.ts |
Tests updated CLI help. |
src/cli/help.ts |
Documents add-on command. |
src/cli/index.ts |
Registers add-on subcommand. |
src/core/analytics.ts |
Adds safe Azure analytics fields. |
src/core/config.test.ts |
Tests Azure configuration. |
src/core/config.ts |
Parses Azure settings. |
src/core/preflight.test.ts |
Tests stack detection. |
src/core/preflight.ts |
Adds stack-aware preflights. |
src/lib/aws/aws-cli-sanitizer.ts |
Shares the CLI tokenizer. |
src/lib/azure/az-file-args.generated.json |
Lists generated file flags. |
src/lib/azure/bicep-install.test.ts |
Tests Bicep installation. |
src/lib/azure/bicep-install.ts |
Downloads verified Bicep binaries. |
src/lib/azure/bootstrap.test.ts |
Tests isolated Azure bootstrap. |
src/lib/azure/bootstrap.ts |
Creates the LocalStack Azure profile. |
src/lib/azure/child-env.test.ts |
Tests environment isolation. |
src/lib/azure/child-env.ts |
Builds allow-listed child environments. |
src/lib/azure/description.test.ts |
Tests tool description limits. |
src/lib/azure/description.ts |
Defines Azure tool guidance. |
src/lib/azure/egress-proxy.test.ts |
Tests egress containment. |
src/lib/azure/egress-proxy.ts |
Proxies and restricts network access. |
src/lib/azure/emulator.test.ts |
Tests emulator health checks. |
src/lib/azure/emulator.ts |
Implements emulator detection. |
src/lib/azure/extension-install.test.ts |
Tests extension installation. |
src/lib/azure/extension-install.ts |
Installs isolated extensions. |
src/lib/azure/extension-map.test.ts |
Tests extension mappings. |
src/lib/azure/extension-map.ts |
Maps commands to extensions. |
src/lib/azure/extension-pins.test.ts |
Verifies embedded pins. |
src/lib/azure/extension-pins.ts |
Embeds extension versions. |
src/lib/azure/local-hosts.ts |
Centralizes allowed local hosts. |
src/lib/azure/loopback-forwarder.test.ts |
Tests Docker forwarding. |
src/lib/azure/loopback-forwarder.ts |
Forwards Docker loopback traffic. |
src/lib/azure/output.test.ts |
Tests Azure result formatting. |
src/lib/azure/output.ts |
Classifies and formats CLI output. |
src/lib/azure/policy.corpus.test.ts |
Validates the sample corpus. |
src/lib/azure/policy.leak.test.ts |
Tests policy leak resistance. |
src/lib/azure/policy.test.ts |
Tests command policy. |
src/lib/azure/policy.ts |
Enforces Azure command containment. |
src/lib/azure/resolve-az.test.ts |
Tests Azure CLI discovery. |
src/lib/azure/resolve-az.ts |
Resolves supported Azure CLI installs. |
src/lib/azure/runner.test.ts |
Tests subprocess execution. |
src/lib/azure/runner.ts |
Runs bounded Azure CLI processes. |
src/lib/azure/runtime-status.test.ts |
Tests lifecycle status mapping. |
src/lib/azure/runtime-status.ts |
Reports Azure runtime status. |
src/lib/azure/services.test.ts |
Tests shared Azure services. |
src/lib/azure/services.ts |
Coordinates Azure dependencies. |
src/lib/azure/spawn.smoke.test.ts |
Smoke-tests real CLI spawning. |
src/lib/azure/testing/tls.ts |
Generates test TLS certificates. |
src/lib/azure/types.ts |
Defines Azure interfaces. |
src/lib/azure/worker-runner.test.ts |
Tests warm workers. |
src/lib/azure/worker-runner.ts |
Manages warm Azure workers. |
src/lib/azure/worker-script.ts |
Implements Python worker protocol. |
src/lib/cli/argv.test.ts |
Tests shared tokenization. |
src/lib/cli/argv.ts |
Implements safe CLI tokenization. |
src/lib/docker/docker.client.test.ts |
Tests stack-aware lookup. |
src/lib/docker/docker.client.ts |
Finds Azure containers safely. |
src/lib/localstack/container-spec.logic.test.ts |
Tests Azure container specs. |
src/lib/localstack/container-spec.logic.ts |
Adds Azure runtime specifications. |
src/lib/localstack/localstack.utils.test.ts |
Tests lifecycle preservation. |
src/lib/localstack/localstack.utils.ts |
Extends lifecycle utilities. |
src/lib/wizard/azure-steps.test.ts |
Tests add-on steps. |
src/lib/wizard/azure-steps.ts |
Implements add-on installation steps. |
src/lib/wizard/cli-args.logic.test.ts |
Verifies unchanged wizard flags. |
src/lib/wizard/prereqs.test.ts |
Verifies wizard prerequisites. |
src/lib/wizard/setup-parity.test.ts |
Enforces setup parity. |
src/tools-tests/localstack-azure-client.test.ts |
Tests the Azure tool. |
src/tools-tests/localstack-management-ports.test.ts |
Tests safe port selection. |
src/tools-tests/localstack-management.test.ts |
Tests Azure lifecycle operations. |
src/tools-tests/stack-guards.test.ts |
Tests tool stack guards. |
src/tools/localstack-app-inspector.ts |
Adds AWS stack guard. |
src/tools/localstack-aws-client.ts |
Adds stack-aware AWS execution. |
src/tools/localstack-aws-replicator.ts |
Adds AWS stack guard. |
src/tools/localstack-azure-client.ts |
Adds Azure CLI tool. |
src/tools/localstack-chaos-injector.ts |
Adds AWS stack guard. |
src/tools/localstack-cloud-pods.ts |
Adds AWS stack guard. |
src/tools/localstack-deployer.ts |
Adds AWS stack guard. |
src/tools/localstack-extensions.ts |
Adds AWS stack guard. |
src/tools/localstack-iam-policy-analyzer.ts |
Adds AWS stack guard. |
src/tools/localstack-logs-analysis.ts |
Restricts AWS-specific analyses. |
src/tools/localstack-management.ts |
Adds Azure lifecycle support. |
src/tools/localstack-snowflake-client.ts |
Adds Snowflake stack guard. |
src/tools/localstack-state-management.ts |
Adds AWS stack guard. |
tests/azure/README.md |
Documents Azure test layers. |
tests/azure/drift/cli-and-extensions.live.test.ts |
Checks CLI and extension drift. |
tests/azure/drift/coverage-and-samples.live.test.ts |
Checks coverage and sample drift. |
tests/azure/drift/drift-helpers.ts |
Provides drift utilities. |
tests/azure/drift/emulator-contract.live.test.ts |
Checks emulator contracts. |
tests/azure/egress-internal/README.md |
Documents isolated egress tests. |
tests/azure/egress-internal/run.sh |
Runs internal-network checks. |
tests/azure/egress.live.test.ts |
Tests live egress containment. |
tests/azure/evals/README.md |
Documents Azure evaluations. |
tests/azure/evals/agent.test.ts |
Tests evaluation agent behavior. |
tests/azure/evals/agent.ts |
Implements evaluation agent loop. |
tests/azure/evals/defects.test.ts |
Tests known evaluation defects. |
tests/azure/evals/fixtures/recorded-verifications.json |
Stores verifier fixtures. |
tests/azure/evals/harness.test.ts |
Tests evaluation harness. |
tests/azure/evals/harness.ts |
Implements evaluation harness. |
tests/azure/evals/keys.test.ts |
Tests API-key handling. |
tests/azure/evals/keys.ts |
Loads and protects API keys. |
tests/azure/evals/record-fixtures.mjs |
Records verifier fixtures. |
tests/azure/evals/run.mjs |
Runs composition evaluations. |
tests/azure/evals/run.test.ts |
Tests evaluation CLI. |
tests/azure/evals/tasks-ta.ts |
Defines tier-A tasks. |
tests/azure/evals/tasks-tapim.ts |
Defines APIM tasks. |
tests/azure/evals/tasks-tb.ts |
Defines tier-B tasks. |
tests/azure/evals/tasks.test.ts |
Tests task definitions. |
tests/azure/evals/types.ts |
Defines evaluation types. |
tests/azure/evals/variants.test.ts |
Tests prompt variants. |
tests/azure/evals/variants.ts |
Builds prompt variants. |
tests/azure/evals/verifiers.test.ts |
Tests task verifiers. |
tests/azure/evals/verifiers.ts |
Implements task verification. |
tests/azure/home-guard.live.test.ts |
Protects the real Azure profile. |
tests/azure/live/harness.ts |
Provides live-test harness. |
tests/azure/live/setup-matrix-full.js |
Selects full matrix. |
tests/azure/live/setup-matrix-pr.js |
Selects PR matrix. |
tests/azure/live/setup-samples-all.js |
Selects all samples. |
tests/azure/live/setup-samples-pr.js |
Selects PR samples. |
tests/azure/live/teardown.ts |
Closes Azure services. |
tests/azure/matrix.live.test.ts |
Executes command matrices. |
tests/azure/matrix/README.md |
Documents matrix format. |
tests/azure/matrix/apimanagement.yaml |
Covers API Management. |
tests/azure/matrix/app.yaml |
Covers managed applications. |
tests/azure/matrix/appconfiguration.yaml |
Covers App Configuration. |
tests/azure/matrix/authorization.yaml |
Covers authorization APIs. |
tests/azure/matrix/cdn.yaml |
Covers CDN APIs. |
tests/azure/matrix/compute.yaml |
Covers compute APIs. |
tests/azure/matrix/containerinstance.yaml |
Covers container instances. |
tests/azure/matrix/containerregistry.yaml |
Covers container registry. |
tests/azure/matrix/containerservice.yaml |
Covers container services. |
tests/azure/matrix/dbformysql.yaml |
Covers MySQL APIs. |
tests/azure/matrix/dbforpostgresql.yaml |
Covers PostgreSQL APIs. |
tests/azure/matrix/documentdb.yaml |
Covers document databases. |
tests/azure/matrix/eventgrid-dataplane.yaml |
Covers Event Grid data plane. |
tests/azure/matrix/eventgrid.yaml |
Covers Event Grid management. |
tests/azure/matrix/eventhub.yaml |
Covers Event Hubs. |
tests/azure/matrix/insights-dataplane.yaml |
Covers Insights data plane. |
tests/azure/matrix/insights.yaml |
Covers monitoring APIs. |
tests/azure/matrix/keyvault.yaml |
Covers Key Vault. |
tests/azure/matrix/kubernetesconfiguration.yaml |
Covers Kubernetes configuration. |
tests/azure/matrix/managedidentity.yaml |
Covers managed identities. |
tests/azure/matrix/network.yaml |
Covers networking APIs. |
tests/azure/matrix/operationalinsights.yaml |
Covers Log Analytics. |
tests/azure/matrix/resourcegraph.yaml |
Covers Resource Graph. |
tests/azure/matrix/resources.yaml |
Covers resource operations. |
tests/azure/matrix/schema.test.ts |
Validates matrix schemas. |
tests/azure/matrix/servicebus-dataplane.yaml |
Covers Service Bus data plane. |
tests/azure/matrix/servicebus.yaml |
Covers Service Bus management. |
tests/azure/matrix/sql.yaml |
Covers SQL APIs. |
tests/azure/matrix/storage.yaml |
Covers storage APIs. |
tests/azure/matrix/web.yaml |
Covers web applications. |
tests/azure/samples-replay.live.test.ts |
Replays official samples. |
tests/azure/samples-shim/README.md |
Documents sample shims. |
tests/azure/samples-shim/az |
Adds Bash Azure shim. |
tests/azure/samples-shim/az-shim.cjs |
Routes shim calls through MCP. |
tests/azure/samples-shim/az.cmd |
Adds Windows Azure shim. |
tests/azure/samples-shim/azd.cmd |
Blocks real Azure Developer CLI. |
tests/azure/samples-shim/powershell.cmd |
Blocks Windows PowerShell. |
tests/azure/samples-shim/pwsh.cmd |
Blocks PowerShell Core. |
tests/azure/samples-shim/quote.cjs |
Quotes shim arguments. |
tests/azure/samples-shim/quote.test.ts |
Tests shim fidelity. |
tests/azure/tools/azure-home-fingerprint.mjs |
Detects profile changes. |
tests/azure/tools/call-tool.mjs |
Calls MCP tools manually. |
tests/azure/tools/live-smoke.mjs |
Runs Azure smoke tests. |
tests/azure/tools/mcp-catalogue.mjs |
Measures tool catalogue size. |
tests/azure/tools/merge-op-catalogue.test.ts |
Tests catalogue merging. |
tests/azure/tools/stdio-client.mjs |
Implements test MCP client. |
tests/docker/image-size.mjs |
Enforces image-size limits. |
tests/docker/l5-image-assertions.sh |
Validates image contents. |
tests/docker/validate-image.mjs |
Extends Docker harness for Azure. |
tests/fixtures/azure/bicep/matrix-identity.bicep |
Adds identity fixture. |
tests/fixtures/azure/bicep/matrix-network.bicep |
Adds network fixture. |
tests/fixtures/azure/bicep/matrix-sub.bicep |
Adds subscription fixture. |
tests/fixtures/azure/bicep/storage.bicep |
Adds storage fixture. |
tests/fixtures/azure/corpus/README.md |
Documents command corpus. |
tests/fixtures/azure/corpus/bash_argv.py |
Generates argv oracle. |
tests/fixtures/azure/corpus/build-corpus.py |
Builds corpus fixture. |
tests/fixtures/azure/corpus/extract.py |
Extracts sample commands. |
tests/fixtures/azure/corpus/samples-az-corpus.json |
Stores command corpus. |
tests/fixtures/azure/drift/certificate-sans.json |
Snapshots certificate SANs. |
tests/fixtures/azure/drift/metadata-endpoints.json |
Snapshots Azure endpoints. |
tests/fixtures/azure/fake-az/echo_argv.py |
Echoes Python arguments. |
tests/fixtures/azure/fake-az/fake-az.mjs |
Simulates Azure CLI behavior. |
tests/fixtures/azure/fake-az/sleeper.mjs |
Simulates lingering children. |
tests/fixtures/azure/fake-worker/azure/__init__.py |
Defines fake Azure package. |
tests/fixtures/azure/fake-worker/azure/cli/__init__.py |
Defines fake CLI package. |
tests/fixtures/azure/fake-worker/azure/cli/core/__init__.py |
Implements fake worker CLI. |
tests/fixtures/azure/leak-commands.json |
Stores leak-resistance cases. |
tests/fixtures/azure/stderr/index.json |
Stores stderr fixtures. |
tests/mcp/azure-offline.spec.mjs |
Tests offline Azure errors. |
tests/mcp/direct.spec.mjs |
Validates Azure tool registration. |
tests/mcp/evals-gemini-azure.spec.mjs |
Runs Azure Gemini evaluations. |
tsconfig.tests.json |
Type-checks Azure tests. |
yarn.lock |
Locks new development dependencies. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
paolosalvatori
left a comment
There was a problem hiding this comment.
Nice work! The containment layers are built with care: the isolated CLI profile, the allow-list child environment, the egress guard that needs no DNS, and the process-group kills. The unit suite is thorough, too. Most of what follows is about where the Azure code meets the existing tools and the configuration that the MCP server page documents. The PR is still a draft.
1. Rules applied
I read these from localstack-pro origin/main (8ae2eff37f). The local clone was 32 commits behind, so every file came from origin/main.
localstack-pro-azure/CLAUDE.md.claude/rules/azure/common/:coding-style.md,hooks.md,patterns.md,security.md,testing.md.claude/rules/azure/python/:cloud-pipeline.md,coding-style.md,hooks.md,patterns.md,security.md,testing.md.claude/rules/azure/bicep/(5.bicepfiles and 1.bicepparamchanged):coding-style.md,hooks.md,patterns.md,security.md,testing.md.claude/rules/azure/scripts/(3.shfiles changed):coding-style.md,hooks.md,patterns.md,security.md,testing.md- Not applicable:
terraform/(no.tffiles) andemulator/(no emulator service code).
All of these are paths:-scoped to localstack-pro-azure/**, and this repo is TypeScript. So I cite a rule only where its principle carries over, and mark it "principle applied".
Extra guidance for this run: check compliance with the MCP server page. I read the live page and its source in localstack-docs (origin/main, src/content/docs/aws/developer-tools/running-localstack/mcp-server.mdx, last changed in 96a87121). The two agree.
2. Existing comments
Copilot left a Balanced review with 4 inline comments and nothing suppressed.
I disagree with its comment on src/core/preflight.ts L98. Making the AWS-only tools refuse a localstack/snowflake container would break documented workflows:
- That image installs
awscliandawscli-local. - The Snowflake docs create S3 buckets against it: stages, Snowpipe and Iceberg tables.
- They also export and import its state (state management).
The docker.client.ts finding below goes the other way.
Its other three comments are fair, and I don't repeat them:
- stderr redaction in
record-fixtures.mjs; install-azure-addons --no-extensionsstill requiringaz;- an empty eval counting as a pass.
3. Findings
Against the MCP server page
| Page definition | With this PR |
|---|---|
All tools require LOCALSTACK_AUTH_TOKEN, and each tool runs pre-flight checks (CLI, container, token) |
Met. localstack-azure-client checks the token first, then the emulator and az. |
localstack-management: service is aws or snowflake (default aws), and envVars are passed on start |
azure is added and the default is unchanged. stop and restart ignore the Azure side-by-side port (HIGH). On restart, an external container's flags now override the env block (LOW). |
LOCALSTACK_HOSTNAME and LOCALSTACK_PORT |
LOCALSTACK_PORT is honoured, but the Azure hints name LOCALSTACK_AZURE_PORT instead (LOW). The Azure tool ignores LOCALSTACK_HOSTNAME (MEDIUM). An explicit LOCALSTACK_PORT now stops the lookup from finding a container that publishes no ports (MEDIUM). |
Any LocalStack configuration variable in the env block is forwarded when localstack-management starts the container |
Not met for Azure: the emulator's LS_AZURE_* settings are dropped (MEDIUM). |
MAIN_CONTAINER_NAME |
Met: it still wins in every lookup. |
MCP_ANALYTICS_DISABLED (default 0, so analytics are on) |
Honoured. With analytics on, though, a password passed as -p<value> reaches PostHog (HIGH). |
localstack-aws-client: runs awslocal in the running container and blocks shell syntax |
The tokenizer move keeps the old behaviour exactly (no options). But the client now refuses the Snowflake emulator's container (HIGH). |
localstack-snowflake-client, localstack-docs, localstack-ephemeral-instances |
Unchanged. |
| Troubleshooting: the start times out after 120 s, and a missing token gives "Auth Token Required" | Both still hold for service: azure. |
Docs page changes this PR needs (not anchorable; the PR body lists most of them)
- A
localstack-azure-clientsection (onecommandparameter), andazurein theservicevalues and description oflocalstack-management. - The Azure CLI (
az2.85 or newer) as an optional prerequisite, andinstall-azure-addons. - The
LOCALSTACK_AZ_*andLOCALSTACK_AZURE_*settings in the configuration reference.server.jsonalso lacksLOCALSTACK_AZ_BICEP_ENV, which the README documents. - A note that the AWS-only tools refuse the Azure emulator, and that
localstack-logs-analysissupports onlyanalysisType: logsthere. -
public/.well-known/mcp/server-card.jsonin localstack-docs, which still describes an AWS-only v0.3.0 server.
CI on this head
-
build (ubuntu-latest)stops at the audit step (fast-uriadvisories), which fails on every recent PR. So the unit suite never ran on Linux in CI. Locally on Linux it passes: 1,572 tests passed and 41 skipped, with 92.6 % line coverage of the Azure code, and both type-checks are clean. -
build (macos-latest)(7 failures) andbuild (windows-latest)(6) fail on this PR's own tests:- the
policy.tsrealpath finding causes the policy and Bicep failures on both, and (judging from theRUNNER~1path in the log) the Windows shim one; runner.test.tsL127 and L422, andquote.test.tsL684-L685, cause the other macOS failures;.gitattributescauses the Windows LF failure.
On Linux with
TMPDIRbehind a symlink, this head fails 6 tests (the macOS set minus the__CF_USER_TEXT_ENCODINGone). With the changes below, it fails none. - the
-
live,L4 samples subsetandsmokefail because the repo'sLOCALSTACK_AUTH_TOKENis not entitled for LocalStack for Azure: the emulator exits with code 55, and Snowflake is refused too. So none of the live results in the PR description ran in CI. The push job indocker.ymlneedssmoke, so after merge no image is published until the secret is entitled or the licence-dependent scenarios stop blocking the push.
How the suggestions were checked: I applied every change below together in a scratch worktree. Both type-checks and prettier are clean, and the full unit suite passes (1,572 passed and 41 skipped, as on this head), also with TMPDIR behind a symlink. Each reproduction uses this head's own functions and fixtures.
The findings, each inline with its evidence and a tested fix:
| Severity | Where | Finding |
|---|---|---|
| HIGH | src/lib/azure/policy.ts L407-L421 |
The realpath check refuses a symlinked workdir's files, and lets .. or a symlinked home past the protected directories |
| HIGH | src/lib/azure/policy.ts L1002-L1003 |
A stuck short-option value, such as -pS3cret, reaches analytics |
| MEDIUM | src/lib/azure/policy.ts L475 |
The URL rule and the egress guard allow every port on localhost |
| HIGH | src/lib/docker/docker.client.ts L249-L253 |
localstack-aws-client now refuses the Snowflake emulator's container |
| MEDIUM | src/lib/docker/docker.client.ts L321-L322 |
With LOCALSTACK_PORT set, a container that publishes no ports is no longer found |
| HIGH | src/tools/localstack-management.ts L315-L320 |
Side by side, stop and restart with service: "azure" act on the AWS emulator |
| LOW | src/tools/localstack-management.ts L348-L352 |
On restart, an external container's flags override the MCP env block |
| MEDIUM | src/lib/azure/resolve-az.ts L349-L350 |
A Windows az behind a non-default WSL automount root gets past the WSL guard |
| MEDIUM | src/lib/azure/worker-script.ts L100-L101 |
The warm worker keeps the first command's log level for up to 200 commands |
| LOW | src/lib/azure/worker-runner.ts L352-L353 |
A cancel during the worker's cold start is lost, and the command still runs |
| MEDIUM | src/lib/localstack/container-spec.logic.ts L34-L38 |
The Azure emulator's own LS_AZURE_* settings in the env block never reach the container |
| MEDIUM | src/lib/azure/loopback-forwarder.ts L265-L266 |
The Azure tool ignores the documented LOCALSTACK_HOSTNAME |
| MEDIUM | .gitattributes L4-L5 |
The matrix YAML files are not pinned to LF, so the new Windows leg fails |
| MEDIUM | src/lib/azure/runner.test.ts L127 |
The cwd check compares the child's physical path with the temp dir as spelled |
| MEDIUM | src/lib/azure/runner.test.ts L422 |
The environment check does not allow for the key that macOS adds itself |
| MEDIUM | tests/azure/samples-shim/quote.test.ts L684-L685 |
The same physical-cwd assumption as runner.test.ts L127 |
| LOW | src/lib/azure/emulator.ts L184-L194 |
The hints name LOCALSTACK_AZURE_PORT where the documented setting is LOCALSTACK_PORT |
| LOW | src/lib/azure/description.ts L20 |
The tool description promises a default location that is never configured |
| LOW | src/tools/localstack-azure-client.ts L100-L104 |
Comments still point at a design document that is not in the repo |
| LOW | scripts/ci/azure-emulator-up.sh L46-L47 |
The health loop keeps polling a container that has already exited |
| LOW | .github/workflows/azure-live.yml L66-L70 |
The ~/.azure fingerprint is taken after the emulator start, so a failed start adds a false home-guard failure |
| LOW | .github/workflows/azure-live.yml L14-L15 |
The path filter misses files that the live suites read |
| LOW | .github/workflows/azure-weekly.yml L23 |
The auth token is set for every job and step of the weekly workflow |
4. Clean files
These changed files have no material findings. Some I read myself; others went through a helper pass, and I verified every point from it that became a finding.
- Tools and core:
src/tools/localstack-aws-client.ts(its regression is filed underdocker.client.ts);- the
requireStackadditions in the other tensrc/tools/*.ts; src/core/preflight.tsandsrc/core/config.ts, apart from the internal references;src/core/analytics.ts,src/cli/index.ts,src/cli/help.ts;src/cli/azure-addons.ts(Copilot's point stands);src/lib/cli/argv.tsandsrc/lib/aws/aws-cli-sanitizer.ts: the AWS client keeps its exact tokenizer behaviour;src/lib/localstack/localstack.utils.ts: its restart carry is filed underlocalstack-management.ts.
- Azure modules:
child-env.ts,runner.ts,bootstrap.ts,services.ts,local-hosts.ts,runtime-status.ts;egress-proxy.ts: its port gap is filed underpolicy.ts;output.ts: the first line that analytics records is onlyexit <code>, <class>;extension-install.ts,extension-map.ts,extension-pins.ts,types.ts,testing/tls.tsandaz-file-args.generated.json;bicep-install.ts: all seven sha256 pins match the v0.47.16 release;src/lib/wizard/azure-steps.ts: the wizard really is unchanged, and onlyinstall-azure-addonsimports this file.
- Packaging, docs and CI:
Dockerfile: the entrypoint is unchanged, and the BicepADD --checksumand the extension pins are enforced;docker/*,manifest.json,README.md,docs/DOCKER.md;server.json, apart from the missingLOCALSTACK_AZ_BICEP_ENV;package.json: no new runtime dependency, andstartandbinare unchanged;.github/CODEOWNERS,.github/actions/azure-live-setup/action.yml,.github/workflows/docker.yml;scripts/ci/merge-op-catalogue.cjs,scripts/install-azure-extensions.mjs,scripts/gen-az-file-args.py,tests/docker/*;.gitignore,.prettierignore,jest.config.js,jest.azure-live.config.js,tsconfig.tests.json,playwright.config.mjs.
- Tests and fixtures:
- no real secrets: only placeholders, all-zero ids and one key the emulator generated;
- the Anthropic eval runs only when invoked explicitly, with a spend cap, and the Gemini one only through
yarn test:mcp:evals:azure; - an AST scan of the 47 non-live test files found no test without a real assertion.
- Deprioritized: the 31 matrix YAML files (covered through
schema.test.ts),tests/azure/evals/tasks-*.ts, the live suites beyond URL and secret greps, and the corpus Python.
5. Verdict
Request changes. There are four HIGH findings, each with a tested suggestion:
- The file rule's realpath check both refuses legitimate files and lets
..or a symlinked home past~/.ssh. - A password passed as
-p<value>is sent to PostHog. - The AWS client refuses the Snowflake emulator.
stopandrestarttake down the AWS emulator when Azure runs beside it.
Separately, the live suites cannot validate anything until the repo's LOCALSTACK_AUTH_TOKEN is entitled for LocalStack for Azure.
…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.
paolosalvatori
left a comment
There was a problem hiding this comment.
I love the diagram, I read the PT with attention tomorrow, thanks!
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.


Motivation
This server covers LocalStack's AWS and Snowflake emulators. This PR adds the LocalStack for
Azure emulator:
localstack-azure-client, runs Azure CLI (az) commands against the LocalStackfor Azure emulator, never real Azure. Whatever
azcan do and the emulator implements works,with
az's own validation and--help; for an operationazhas no command for,restwith arelative URL reaches the emulator.
localstack-managementmanages the Azure emulator withservice: "azure": start, stop,restart and status.
Why the Azure CLI:
azalready covers every Azure API, so new emulator coverage reaches userswithout a code change here, and one tool keeps the catalogue small.
Why the user's own
az, the way the Snowflake tool usessnow: the Azure emulator image shipsno client CLI (no
az, Bicep orazlocal), unlike the AWS image, whoseawslocalthe AWS tool runsinside the container. So the tool runs the
azon the user's machine, in an isolated profile, andsays 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;localstack-azure-clientchecks each command against its policy, then runs the Azure CLI on thesame host in the tool's own profile. Every connection
azmakes goes through the egress guard,which lets it reach only the emulator.
localstack-managementstarts, stops and restarts theemulator's container through the Docker Engine API.
The Azure tool:
localstack-azure-clientOne input,
command: anazcommand without the leadingaz.az(src/lib/azure/resolve-az.ts): MSI, pip, Homebrew and deb installs, 2.85 ornewer. It spawns the CLI's own Python (
-X utf8 -IBm azure.cli), never through a shell or a.cmdlauncher.LOCALSTACK_AZ_PATHoverrides the lookup.bootstrap.ts): the tool's ownAZURE_CONFIG_DIR(
~/.localstack/azure/mcp-config-<port>) with aLocalStackcloud and a dummy login, plus aprivate home and temp folder. The user's own
azlogin is never used or changed.src/lib/azure/):policy.tsrefuses shell syntax, logins, cloud and config changes, extension installs,upgrade, and commands that open a browser, shell or tunnel or run Docker. File argumentsmust stay inside
LOCALSTACK_AZ_WORKDIR, and never reach~/.azure,~/.ssh,~/.kubeor~/.docker, checked as the OS will open them (through symlinks, junctions and 8.3 names).child-env.tsgivesazan allow-list environment: the user'sAZURE_*,ARM_*, proxy andCA variables never reach it.
egress-proxy.tsroutes every connectionazor Bicep makes through a local proxy that allowsonly 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.comURLsgiven to
restare rewritten to relative ones.output.ts):az's own error text, a failure class in thefirst line (
not-implemented,extension,bicep-missing,egress-refused, …) and a hint..bicepand.bicepparamdeployments work (LOCALSTACK_AZ_BICEP_PATH, then thetool's own folder, then
PATH).LOCALSTACK_AZ_RUNNER=worker).Setup, the Snowflake way
initwizard is unchanged.npx -y @localstack/localstack-mcp-server install-azure-addonsinstalls the 26 pinnedazextensions and Bicep 0.47.16 (sha256-checked) into the tool's own folders, never into
~/.azure(--no-extensions,--no-bicep).azanswers with install commands for Windows, macOS and Debian/Ubuntu, as a missingsnowdoes; a missing extension or Bicep namesinstall-azure-addons.LOCALSTACK_AZ_*settings, and "How the Azure toolstays local".
Shared code (AWS and Snowflake run through it)
requireStack,src/core/preflight.ts): atool 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). TheAWS-only tools refuse on the Azure emulator.
localstack-management:service: "azure";statusnames the container it found;restartrefuses, before stopping anything, a container whose state folder this machine cannotmount;
restartof an externally started container (lstk,docker run) keeps thatcontainer's own env flags, for AWS and Snowflake too, unless this server's
envblock setsthem. A container this server started behaves as before. With the Azure emulator beside an AWS
one (
LOCALSTACK_AZURE_PORTset),statusandstopact on the Azure container, andrestartrefuses before stopping anything. A start forwards the Azure emulator's own settings
(
LS_AZURE_*,MSSQL_ACCEPT_EULA, …) from theenvblock.lstk's names; withLOCALSTACK_PORTset ittakes 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.
src/lib/cli/argv.ts, shared by both clients; the AWS clientcalls it without options, so its behaviour is unchanged.
@anthropic-ai/sdkandyamlare dev dependencies).Docker image
az2.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.mjsgates 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(serviceazure) says so when it starts the emulator on a namedvolume.
CI
ci.yml: unit tests on Ubuntu, Windows and macOS with a 90 % line-coverage gate on the Azurecode, type-checks of the test code, prettier on the changed files, and an
azspawn smoke test.docker.yml: image assertions (amd64 and arm64), the size gate, and the image against realemulators: AWS and Snowflake, then the Azure stage in a run of its own.
azure-live.yml(new; path-filtered PRs andmain): a real LocalStack for Azure emulator perjob, 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.
LOCALSTACK_AUTH_TOKENsecret, so its licence must cover AWS, Snowflakeand 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 officialsamples, drift gates, and
az2.85 and the latest. No job calls a model API: the model evalsrun locally only.
azure-live.ymlonmain, opens or updates a tracking issuethat 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,.cmdfiles CRLF.After the first review
for files that do not exist yet, and for a
..after a link.-pSecret).LOCALSTACK_PORTset, acontainer that publishes no port is still found.
stop(serviceazure) acts on the Azure container, andrestartrefuses before stopping anything. On restart, this server's
envblock wins over the oldcontainer's flags. A start forwards the Azure emulator's
LS_AZURE_*settings.LOCALSTACK_AZ_PATHruns by its own path. A Windowsazbehind any WSL mountroot is refused.
LOCALSTACK_HOSTNAMEis tried for the emulator; the hints nameLOCALSTACK_PORT.install-azure-addons --no-extensionsworks withoutaz;server.jsonlistsLOCALSTACK_AZ_BICEP_ENV; the tool description no longer claims a default location.that need it; the live path filter covers
src/core,src/cliand the Azure fixtures.Tests
link (as macOS's
/varand Windows 8.3 paths are); line coverage of the Azure code 92.8 %(gate: 90 %).
tests/azure/README.mdlists the layers):the command matrix (393 cases), the egress checks, the official samples through an
azshim, theMCP 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.live(the matrix subset, the egress checks and theharness 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
localstack/localstack-azure-samples,pinned at 5ae6984.