Skip to content

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

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

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

Conversation

@DrisDary

@DrisDary DrisDary commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds localstack-azure-client, which runs Azure CLI (az) commands against the LocalStack for Azure emulator, and service: "azure" in localstack-management (start, stop, restart and status).

  • The tool runs the user's own 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.
  • Commands are split without a shell. A short deny list refuses commands that would re-point or log out the profile, install software, or open browsers, shells and streams. .. path traversal is refused, as in the AWS client. A management.azure.com URL given to rest becomes a relative path.
  • Extensions come from the user's own extension directory. A command from a missing extension gets a hint naming it.
  • az is 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.
  • Settings: LOCALSTACK_AZURE_ENDPOINT, LOCALSTACK_AZURE_IMAGE_NAME, LOCALSTACK_AZ_PATH, LOCALSTACK_AZ_CONFIG_DIR, LOCALSTACK_AZ_WORKDIR and LOCALSTACK_AZ_TIMEOUT_SECONDS. The tool reaches the emulator through LOCALSTACK_HOSTNAME and LOCALSTACK_PORT, as the other tools do.
  • The Docker image does not include az yet; inside it, the tool says so.

Tests

  • Unit tests for the policy and tokenizer, the settings, finding az, the child environment, the runner, the profile bootstrap, the emulator checks and the output, plus the tool itself and service: azure in localstack-management.
  • tests/mcp/azure-offline.spec.mjs runs in mcp-direct-tests, without an emulator.
  • .github/workflows/azure-smoke.yml (on demand and weekly) starts the emulator with localstack-management and runs a short tour of az commands 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.

  • Egress guard: keeps az from reaching hosts other than the emulator, with the threat model written down.
  • Docker image: 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).
  • Lifecycle extras: the AWS and Azure emulators side by side, and settings kept across a restart.
  • Warm worker: faster az calls, with a benchmark script.
  • Shared tokenizer: one tokenizer for the AWS and Azure clients, and file containment if the team wants it.

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 Outdated
Comment thread src/lib/azure/policy.ts Outdated
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 HarshCasper left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.ts without the version special case, the test envelope and requireStack.
  • bootstrap.ts keeps CLI_CONFIG, DUMMY_LOGIN, cloudConfigJson, the five az calls and the marker file. The lock directory, the lease files, the version-check seed and the self-heal can go.
  • runner.ts and child-env.ts keep the spawn core (it fixes real problems in command-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.ts keeps LOCALSTACK_AZ_PATH, a PATH walk, the az.cmd to python.exe mapping and the version probe. The launcher-text parsing per installer, the WSL guard and the local az version answer can go.
  • policy.ts keeps 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), the management.azure.com rewrite, and the file rule for existing files under the workdir. The two generated JSON tables, scripts/gen-az-file-args.py and leak-commands.json can go.
  • emulator.ts and runtime-status.ts as they are, minus the guard-off branch.
  • output.ts cut 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.ts into src/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 of LS_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

  1. Tests of the emulator, az and the samples repo. tests/azure/matrix (393 cases over 29 provider files, 61 dated known_gap entries, 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 in tests/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 drive az directly. What this repo needs is one short live smoke, around ten commands through the tool, on workflow_dispatch and a weekly schedule.

  2. Snapshots of az internals that only the weekly job keeps honest. az-command-words.generated.json and az-file-args.generated.json (2,926 lines generated from azure-cli internals), 26 extension pins taken against 2.90.0, the Bicep version and sha256 repeated in the Dockerfile, ci.yml, the composite action, docker.yml and bicep-install.ts, 43 stderr fixtures recorded on Windows from one az version, and about 35 regexes over az stderr wording behind 25 failure classes in output.ts. The AWS client has two regexes. Every one of these goes stale as soon as az changes, and nothing in the normal CI run catches it.

  3. Changes to code every AWS user runs.

    • src/core/preflight.ts and src/core/config.ts now import from src/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 under src/lib/azure.
    • requireStack in twelve tools. For the Azure tool it repeats the edition check emulator.ts already 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 refuses localstack-snowflake-client, where main would at least run snow. 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: with LOCALSTACK_AZURE_PORT set and no Azure emulator, status and the tool both say to run start, and start refuses with "Conflicting Azure port settings". Each of these can come back on its own with a scenario that fails without it.
  4. CI and process. ci.yml moves every PR to a three-OS matrix with a 90% coverage gate. azure-live.yml starts a real emulator on each PR (11 minutes, plus 16 for the samples job on this run). azure-weekly.yml has eight jobs and files issues assigned to me. CODEOWNERS adds the team and me to every Azure path. And the push job in docker.yml now depends on the one LOCALSTACK_AUTH_TOKEN being entitled for Azure, so no image publishes when it isn't. For this PR: ci.yml stays as on main, one dispatch-plus-weekly smoke job replaces the two workflows, no CODEOWNERS, no auto-filed issues.

  5. 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 az and 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 be LOCALSTACK_HOSTNAME, as for the other tools.

  6. The warm worker: worker-runner.ts, worker-script.ts and 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 real az, and the README's "about 5x faster" has no benchmark in the repo. It can come back with one.

  7. install-azure-addons and 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.ts points AZURE_EXTENSION_DIR at a private directory and the bootstrap sets extension.use_dynamic_install=no, so whatever the user has installed with az extension add is invisible. Let the child inherit the user's extension directory read-only, keep the hint map in extension-map.ts (about 95 lines), and document Bicep with brew, winget or az bicep install.

  8. 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.

@DrisDary

DrisDary commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

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.ts without the version special case, the test envelope and requireStack.
  • bootstrap.ts keeps CLI_CONFIG, DUMMY_LOGIN, cloudConfigJson, the five az calls and the marker file. The lock directory, the lease files, the version-check seed and the self-heal can go.
  • runner.ts and child-env.ts keep the spawn core (it fixes real problems in command-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.ts keeps LOCALSTACK_AZ_PATH, a PATH walk, the az.cmd to python.exe mapping and the version probe. The launcher-text parsing per installer, the WSL guard and the local az version answer can go.
  • policy.ts keeps 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), the management.azure.com rewrite, and the file rule for existing files under the workdir. The two generated JSON tables, scripts/gen-az-file-args.py and leak-commands.json can go.
  • emulator.ts and runtime-status.ts as they are, minus the guard-off branch.
  • output.ts cut 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.ts into src/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 of LS_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

  1. Tests of the emulator, az and the samples repo. tests/azure/matrix (393 cases over 29 provider files, 61 dated known_gap entries, 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 in tests/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 drive az directly. What this repo needs is one short live smoke, around ten commands through the tool, on workflow_dispatch and a weekly schedule.

  2. Snapshots of az internals that only the weekly job keeps honest. az-command-words.generated.json and az-file-args.generated.json (2,926 lines generated from azure-cli internals), 26 extension pins taken against 2.90.0, the Bicep version and sha256 repeated in the Dockerfile, ci.yml, the composite action, docker.yml and bicep-install.ts, 43 stderr fixtures recorded on Windows from one az version, and about 35 regexes over az stderr wording behind 25 failure classes in output.ts. The AWS client has two regexes. Every one of these goes stale as soon as az changes, and nothing in the normal CI run catches it.

  3. Changes to code every AWS user runs.

    • src/core/preflight.ts and src/core/config.ts now import from src/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 under src/lib/azure.
    • requireStack in twelve tools. For the Azure tool it repeats the edition check emulator.ts already 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 feat(azure): add the localstack-azure-client tool and Azure lifecycle support #79; and with an AWS container running it now refuses localstack-snowflake-client, where main would at least run snow. 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: with LOCALSTACK_AZURE_PORT set and no Azure emulator, status and the tool both say to run start, and start refuses with "Conflicting Azure port settings". Each of these can come back on its own with a scenario that fails without it.
  4. CI and process. ci.yml moves every PR to a three-OS matrix with a 90% coverage gate. azure-live.yml starts a real emulator on each PR (11 minutes, plus 16 for the samples job on this run). azure-weekly.yml has eight jobs and files issues assigned to me. CODEOWNERS adds the team and me to every Azure path. And the push job in docker.yml now depends on the one LOCALSTACK_AUTH_TOKEN being entitled for Azure, so no image publishes when it isn't. For this PR: ci.yml stays as on main, one dispatch-plus-weekly smoke job replaces the two workflows, no CODEOWNERS, no auto-filed issues.

  5. 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 az and 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 be LOCALSTACK_HOSTNAME, as for the other tools.

  6. The warm worker: worker-runner.ts, worker-script.ts and 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 real az, and the README's "about 5x faster" has no benchmark in the repo. It can come back with one.

  7. install-azure-addons and 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.ts points AZURE_EXTENSION_DIR at a private directory and the bootstrap sets extension.use_dynamic_install=no, so whatever the user has installed with az extension add is invisible. Let the child inherit the user's extension directory read-only, keep the hint map in extension-map.ts (about 95 lines), and document Bicep with brew, winget or az bicep install.

  8. 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:

  1. LOCALSTACK_AZURE_PORT: with side-by-side mode gone, a second port setting is what causes the status/start loop you found. I'd drop it and have the Azure tool use LOCALSTACK_PORT, as the Snowflake tool does (one emulator at a time). If you'd rather keep it, I'll make start launch the Azure emulator on that port so status and start agree.

  2. Analytics: the generated command-word list is what keeps command_path free of user values (Copilot's finding on this PR: az find <term> recorded the term). Without it, I'd drop command_path and keep flag_names and policy_outcome, which are value-free.

  3. File rule: keeping only "existing files under the workdir" leaves downloads and other write targets unchecked, since the file doesn't exist yet (e.g. storage blob download --file <anywhere>, download-batch --destination <anywhere>). I'd keep a short hand-reviewed list of those write flags (about ten), not generated from az, so they stay inside the workdir. OK, or would you rather leave that for round 2?

  4. Shared tokenizer: keeping it also keeps the AWS sanitizer calling it (+7/-58 in aws-cli-sanitizer.ts, behaviour unchanged for AWS). Keep that in this PR, or leave the AWS file as on main?

Everything that comes out goes to a parked branch with a draft PR, so it can come back in smaller PRs with evidence.

@HarshCasper

Copy link
Copy Markdown
Member

Thanks, all four are fair questions. Here is where I land on each.

  1. Drop LOCALSTACK_AZURE_PORT. The Azure tool reads LOCALSTACK_PORT, as the Snowflake tool does, one emulator at a time. That makes it six settings instead of seven, and the status/start loop goes away on its own. The setting can come back with side-by-side mode in its own PR. While you are in there, the health URL can be built from LOCALSTACK_HOSTNAME and LOCALSTACK_PORT like LOCALSTACK_BASE_URL in config.ts, instead of 127.0.0.1. That is also what makes the forwarder unnecessary inside the image.

  2. Record command the way the AWS client does: the same allow-list entry in analytics.ts, the same 200-character cut. That drops command_path, flag_names and policy_outcome, and the word list with them. The value-free guarantee was stricter than anything the repo does for the other clients, and I would rather have one rule for both clients than a catalogue of az internals to keep in sync. Copilot's finding goes away with the field.

  3. No write-flag list in this PR. For file arguments, match the AWS client: refuse .. traversal and otherwise let az do what the user asked. That also replaces the existing-file rule I listed under what stays, so the policy module gets smaller than I described. If the team wants read or write containment later, it comes as its own PR with the cases it defends against.

  4. Leave aws-cli-sanitizer.ts as on main. Put the tokenizer the Azure tool needs under src/lib/azure for now, with the three options it turns on. Once this PR is in, send the extraction as its own PR. With the AWS test file unchanged it is a quick review, and both clients share one tokenizer from then on.

The parked branch with a draft PR is the right way to keep the rest. Go ahead with the cut.

@DrisDary
DrisDary marked this pull request as draft October 8, 2026 19:26
@DrisDary

DrisDary commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @HarshCasper, the cut is pushed. This PR is now the Azure tool, service: "azure" in localstack-management, and their tests: +3,062 −25 across 42 files, a third of it tests. 44 of those lines (the lockfile bumps and the docs-outage tolerance) are also in a separate PR to main, so they drop out once that merges, leaving about 3,040.

Per your review

  • CI is back to main's workflows. The live, weekly and per-service jobs are gone. A small azure-smoke.yml (on demand and weekly) starts the emulator through localstack-management and runs about ten az commands through the tool.
  • The Dockerfile, docs/DOCKER.md and the image harness are main's. The image ships the tool but no az yet, and the tool says so there.
  • core/preflight.ts, core/config.ts, docker.client.ts, the CLI and the 11 non-Azure tools are main's again, and requireStack is gone. The Azure settings and preflights live in src/lib/azure/, so the other tools' chunks are back to their previous size.
  • Removed: the egress guard, the warm worker, the extension and Bicep installers and pins, the generated tables, the corpus, leak and spawn tests, the live suites and fixtures, the evals and the Anthropic SDK.
  • D1: one emulator at a time. LOCALSTACK_AZURE_PORT is gone. The tool uses LOCALSTACK_PORT, and its health URL is built from LOCALSTACK_HOSTNAME and LOCALSTACK_PORT like LOCALSTACK_BASE_URL. Six settings remain.
  • D2: analytics records command with the same allow-list entry as the AWS client.
  • D3: no write-flag list and no existing-file rule. .. traversal is refused as in the AWS client; the rest is up to az.
  • D4: aws-cli-sanitizer.ts is untouched. The Azure tokenizer is src/lib/azure/argv.ts, with its three options always on.
  • Extensions come from the user's own extension directory (az extension add). A command from a missing extension gets a hint naming it.

Small things I kept, so you can push back on them

  • A Windows az found through WSL is skipped. WSL doesn't forward AZURE_CONFIG_DIR, so the bootstrap would rewrite the user's real Windows profile.
  • LOCALSTACK_AZ_CONFIG_DIR may not overlap ~/.azure or AZURE_CONFIG_DIR, for the same reason.
  • The emulator check resolves the endpoint's name once per emulator session. Without the guard, az resolves *.localhost.localstack.cloud itself, and some routers block those answers (DNS-rebinding protection), so the check names the cause instead of a TLS or connection error.
  • status with service: snowflake now also names a running Azure container, as it already did for an AWS one.
  • An az still running when the server stops is killed with it: on exit (the client closed stdin) and, on POSIX, on SIGTERM, SIGINT or SIGHUP while a command runs. Without it, a command the client gave up on keeps running and changing the emulator: start a long command (group wait --created --timeout 120), close the client, and the az process is still there.
  • Servers that share the default config dir (two clients, or several sessions) set the profile up one at a time, behind a lock directory. az cloud and az config rewrite the same config file, so concurrent bootstraps drop each other's settings: start three servers on one fresh profile and send each a command, and most fail at az cloud register or az login. The bootstrap now also registers or updates the cloud in one step, so it makes one az call fewer.

These two and their tests are why the total sits about 40 lines over 3,000 once the main PR merges. If you'd rather keep this PR under it, I'll move them into the first follow-up.

Parked: the full version is in draft #88, not for merge.

Still to do after this merges
Follow-ups, each its own small PR with steps that reproduce the case it handles:

  1. The egress guard (CONNECT-only), with the threat model written down: a connection string given in a command can send az's data-plane requests, with the file's content, to any host.
  2. The Docker image with az, Bicep and the loopback forwarder. Inside the image, the ARM and data-plane names resolve to 127.0.0.1, and the emulator's certificate isn't valid for host.docker.internal. Plus the image-size trade-off.
  3. requireStack, as tests that fail on main: with only the Azure emulator running, the AWS client answers "Unknown error"; with an AWS emulator running, the Snowflake client reports a licence error.
  4. Lifecycle extras (side-by-side mode, carrying env across a restart), each with the scenario that fails without it.
  5. The warm worker, with a benchmark script in the PR.
  6. One tokenizer shared by the AWS and Azure clients, and file containment if the team wants it.

@DrisDary
DrisDary marked this pull request as ready for review October 8, 2026 19:42
@DrisDary
DrisDary requested a review from HarshCasper October 8, 2026 19:42
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.

3 participants