Changelog¶
All notable changes to azemu will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
Added¶
redis-cachescenario: a Redis cache with its connection string stored in Key Vault, the common pattern of provisioning a managed cache and reading its connection details as a secret instead of embedding them. Runs end to end against azemu viaterraform testand exercises the RedislistKeysendpoint and the Key Vault secret data plane.- CDN content data plane. The CDN was control-plane only: azemu stored the
endpoint and computed its
{name}.azureedge.nethost but served nothing through it, so any scenario asserting a CDN read path had to fetch the Blob origin directly. A request to the endpoint host{name}.azureedge.net(on the ARM port) is now reverse-proxied to the endpoint's Blob origin (Azurite, path-style), streaming the body back with the origin'sContent-TypeandCache-Controlpassed through unchanged, which is how Azure CDN honours origin metadata by default.GETandHEADonly. The host is multiplexed on the ARM port behind a new*.azureedge.netcert SAN, mirroring the Key Vault{vault}.vault.localhostdata-plane pattern. The capability is generic (it serves any blob path, unaware of content) so it upgrades thestatic-sitescenario and any future CDN-fronted design. Delivery-rule TTL overrides are a tracked follow-up. ota-deliveryscenario: a server-less, static-file OTA delivery design validated end to end against azemu (Phase 8.7.1). A build step signs an Expo Updates Protocol v1 manifest with a Key Vault key and writes immutable artefacts to Blob; a release step promotes a version by a server-side blob copy and writesrollout.json; the CDN content data plane serves the signed manifest and assets. The scenario's ownfixturegentool (written from the public protocol shape, not a port of any pipeline) drives publish, promote, and a read-path verify that checks the multipartContent-Type, the cache TTLs, and the manifest signature. CI runs the ARM-halfterraform test;make ota-deliveryruns the full local loop against Azurite.- Documentation site: a "License & Forking" page covering the MIT licence, responsible forking (rebrand, keep attribution, register derived providers under your own namespace), and the Terraform BUSL vs OpenTofu MPL licensing difference. Linked from the home page, README, and contributing guide.
- OpenTofu is documented as a supported drop-in alongside Terraform across the install guide, "How It Works", the home page, and the README.
Changed¶
- Documentation site is production-ready and contribution-focused: per-page "edit on GitHub" links, social/community links, a footer, a copyright notice, and badges. The home page and contributing guide now open with a clear invitation to contribute and point at good first issues and Discussions.
- Reframed the project narrative (README, ROADMAP, site home and roadmap) to
be tool-first and demand-driven: azemu emulates plain Azure (ARM, metadata,
OIDC) and drives multiple toolchains (
tf/pulumi/kubectl/pythonsubcommands), and coverage grows from user feature requests rather than a published master plan. Added a clear "tell us what you need" feedback path (feature requests + discussions) anchored on the parity matrix. The fidelity-first engineering bar and the existing non-goals are unchanged. - Redesigned the docs site home page for clarity: a clean "what azemu emulates today" table, an "examples that solve real use cases" section linking the six working scenarios, a short "what we are aiming for", and a single "get involved" call to action with a GitHub-star nudge.
Fixed¶
- Docs site no longer references the removed
scripts/aztfwrapper. The home-page quick start usesterraform -chdir, and the install guide documents theazemu tfsubcommand that replaced it. terraform destroyno longer hangs. The metadataresourceManagerendpoint dropped its trailing slash (https://localhost:<port>). With the trailing slash, the azurerm provider built DELETE URIs as//subscriptions/...; the hashicorp/go-azure-sdk delete poller GETs the resource and waits for a404, but its URI parser treats the leading//host-relatively, drops the resource name, and polled the parent list (200) until the 30-minute delete timeout. Removing the trailing slash makes the poller GET the real resource, which returns404immediately (azemu deletes synchronously), so destroy completes at once. Verified end-to-end against the real azurerm provider. See TODO.md M9.- Load balancer probes and load balancing rules now round-trip. They have no
standalone ARM create operation, so the azurerm provider (
azurerm_lb_probe,azurerm_lb_rule) writes them inline via the parent Load Balancer PUT, butputLBdropped those inline arrays, so the provider saw the probe vanish after apply (Provider produced inconsistent result after apply: ... Root object was present, but now absent).putLBnow persists inlineprobes/loadBalancingRulesas child entries thatgetLBembeds, writing them only after the parent LB store write succeeds so a failed PUT cannot orphan children. The arrays are reconciled, not just appended: a PUT that includes the array with an element removed (anazurerm_lb_probe/azurerm_lb_ruledestroy, which is a read-modify-write on the parent LB) deletes the stale child, while a PUT that omits the array entirely (a plainazurerm_lbapply) leaves existing children untouched. See TODO.md M8. - Async DELETE polling now resolves instead of hanging. Every resource's
202 AcceptedDELETE set aLocation: /subscriptions/{sub}/operationresults/{id}header, but nothing served that path, so the azurerm provider polled a dead URL until its 30-minute delete timeout (polling after Delete: context deadline exceeded) and the relative URL also failed the older go-autorest CDN poller outright (StatusCode=0). Newinternal/arm/operations.goadds theoperationresultsendpoint (returns{"status":"Succeeded"}; azemu deletes synchronously) and builds an absoluteLocationcarrying the request'sapi-version. Each DELETE advertises the operation via bothAzure-AsyncOperation(which the azurerm poller prefers and which expects the{"status":"Succeeded"}body the endpoint returns) andLocation. This affected every async-delete resource (NSG, LB and children, CDN, subnet, DNS zone, VNet, AKS, Redis, App Gateway, Public IP, resource group). See TODO.md M7. - AKS:
POST .../managedClusters/{name}/listClusterUserCredentialandlistClusterAdminCredentialare now implemented, returning a kubeconfig that the azurerm provider parses intokube_config/kube_admin_config. Previously both fell through to the unhandled-route handler and its 501 NotImplemented failed everyazurerm_kubernetes_clusterapply (aks-workload scenario). - static-site scenario: azurerm pinned to
>= 4.0, < 4.35. From v4.35.0 the provider blocks creating classic CDN resources after the 2025-10-01 deprecation date (client-side wall-clock check, no opt-out), failing the scenario before any request reaches azemu. Migration to Front Door is tracked in TODO.md. - All Terraform scenarios (and the top-level
examples/terraform) now pin azurerm to>= 4.0, < 4.35, andmake tf-test*no longer passes-upgrade. Previouslyinit -upgradepulled the newest azurerm on every run, so CI silently drifted onto provider versions azemu was never validated against. azurerm 4.78+ added anazurerm_storage_containeraccount-ID check requiring acore.windows.netblob-endpoint suffix, which azemu's Azurite path-style endpoints (per design note 1) do not satisfy; that broke the ado-pipeline scenario. Pinning makes scenario CI deterministic. See TODO.md M6. - CI:
make tf-test-scenariosnow runs every scenario and reports a pass/fail summary instead of aborting on the first failure. The old fail-fast loop hid the status of every scenario alphabetically after the first broken one.
Added (Key Vault keys, sign-only RSA)¶
- Key Vault keys data plane: create/import RSA keys (2048/3072/4096) with
versioning, public-JWK GET, list and list-versions, PATCH updates,
delete with cascade, and the
signoperation (RS256, RSASSA-PKCS1-v1_5 over a SHA-256 digest), including the versionless form that resolves the current key version. Signatures verify against the returned public JWK. (azurerm_key_vault_key) - Host-based Key Vault data-plane routing:
vaultUriis nowhttps://{vault}.vault.localhost[:port]/and root-level/keysand/secretsroutes resolve the vault from the Host header. The azurerm provider requires both the{name}.vault.**host shape (KeyVaultIDFromBaseUrl) and vault-less nested-item URLs (ParseNestedItemID); the previous path-style ids brokeazurerm_key_vault_secretandazurerm_key_vault_keyread-back. Path-style routes under/keyvault/{vault}/remain for raw clients. - Subscription-wide Resources list (
GET /subscriptions/{sub}/resources) with$filter=resourceType eq '...'support; used by the provider to map a vaultUri back to the vault ARM ID. - Key Vault soft-delete purge stubs:
POST .../deletedvaults/{name}/purge(the shapevaults.VaultsClient#PurgeDeletedactually sends) and data-planeDELETE /deleted{keys,secrets}/{name}returning 204. - Storage
blobServices/defaultPUT: theblob_propertiesblock onazurerm_storage_accountno longer fails with 405; properties round-trip on GET. AZURITE_ACCOUNTSpre-registration indocker-compose.yml(devstoreaccount1,examplestorage001,azemuotasa) plus a SETUP.md section on registering Terraform-chosen storage account names.examples/terraform/scenarios/ota-updates/: OTA update pipeline scenario (storage account + key vault + RSA signing key) with a documented publish-time sign call.azurerm_key_vault_keyexample inexamples/terraform/keyvault.tfwith akey_vault_key_idoutput and test assertion.
Changed (Key Vault keys, sign-only RSA)¶
- The self-signed TLS certificate now carries a
*.vault.localhostSAN. Existing persisted bundles are regenerated automatically on startup and must be trusted again (security add-trusted-cert ...on macOS; see docs/TROUBLESHOOTING.md). - Key Vault nested-item ids (secret
id, keykid) moved from{kvEndpoint}/keyvault/{vault}/...tohttps://{vault}.vault.localhost[:port]/....
Added (Phase 7.7: Azure Cache for Redis)¶
Microsoft.Cache/RedisCRUD + HEAD + list-by-RG + list-by-sub. SKU validated insideproperties.sku(Basic, Standard, Premium with family C/P and capacity ranges); Premium-only properties (shardCount, subnetId, RDB/AOF persistence keys) rejected on Basic/Standard. (azurerm_redis_cache)POST .../Microsoft.Cache/Redis/{name}/listKeys: returns deterministic dev keys (azemu-dev-primary-key,azemu-dev-secondary-key). The primary value matches the Redis sidecar's--requirepassso SDK clients authenticated via the ARM response succeed against the data plane.AZEMU_REDIS_ENDPOINTenv var (defaultredis://azemu-redis:6379). azemu derives thehostNamefield on Redis ARM responses from the URL host so callers connect to the configured sidecar instead ofredis.cache.windows.net.docker-compose.yml: optionalredisservice (redis:7-alpine) on theredisprofile so default users see no extra container; healthcheck viaredis-cli pingwith the dev password.redisCache: "redis.cache.windows.net"suffix in/metadata/endpoints, pinned byTestMetadata_CanonicalSuffixNames.examples/terraform/redis_cache.tfplusredis_cache_id/redis_cache_hostnameoutputs and aterraform testassertion.docs/design-notes/0003-add-azure-cache-for-redis.mdpromoted from Proposed to Implemented (Implemented date: 2026-04-28).website/docs/resources/design-notes/0002-...mdand0003-...mdmirrors plus mkdocs nav entries (closes the website-mirror gap from TODO.md for design notes 2 and 3).docs/SETUP.mdandwebsite/docs/reference/setup.md: newAZEMU_REDIS_ENDPOINTenv-var row and a "Redis sidecar (optional)" section explaining the compose profile,--requirepasscontract, and host-mode setup.
Added (Phase 2 secondary coverage pass)¶
cmd/azemupackage now has test coverage (was 0%). New test files cover: pure-logic helpers (stringSlice,credentialMatches,formatUptime,statusIcon,setEnvDefaults,resolveCertFile,tlsInsecureConfig,insecureHTTPClient,snapshotDir), HTTP helpers (probeHealth,waitForHealth), andResolveFederatedIdentity.internal/ado:endpointBelongsToProjectcoverage 33%→100%;writeADOJSONencode-failure path covered.internal/arm: DNSdnsZoneResponseproperty passthrough, FIC validation-error paths, Key Vault secret attribute passthrough and custom-attribute round-trip.- Overall test coverage: 75.6%→78.3%. 568 tests pass with
-race.
Fixed¶
test/integration/build resurrected.arm.NewRouterhad grown a second parameter in Phase 7 (KeyVaultEndpoint) without updating the integration harness; this PR adds the missing endpoints (Azurite, Key Vault, Redis) to bothbuildFullServerandbuildProductionLikeServerand corrects two pre-existing assertions that compared against real-Azure hostnames (integrationacct.blob.core.windows.net,mytestvault.vault.azure.net) instead of the configured test endpoints.
Added (Phase 7: Storage, Key Vault)¶
Microsoft.Storage/storageAccountsCRUD + HEAD + list-by-RG + list-by-sub. Name uniqueness check across subscription. SKU/kind at top level; soft-delete and access-tier defaults. (azurerm_storage_account)Microsoft.Storage/storageAccounts/blobServices/containersCRUD + HEAD + list. Parent-existence check; cascade delete when account is deleted. (azurerm_storage_container)POST .../storageAccounts/{name}/listKeys: returns Azurite's well-known development account key so SDK clients authenticate against the Azurite sidecar without extra configuration.primaryEndpointsblock in storage account responses now returns path-style Azurite endpoint URLs (blob:10000, queue:10001, table:10002) derived fromAZEMU_AZURITE_ENDPOINT.AZEMU_AZURITE_ENDPOINTenv var (defaulthttp://azurite:10000). azemu derives queue and table base URLs from this single knob.docker-compose.yml:azuriteservice (mcr.microsoft.com/azure-storage/azurite) with ports 10000-10002, named volume, healthcheck, anddepends_on(condition:service_healthy) so azemu starts only after Azurite is ready.Microsoft.KeyVault/vaultsCRUD + HEAD + list-by-RG + list-by-sub.vaultUricomputed ashttps://{name}.vault.azure.net/; SKU defaults tostandard; soft-delete defaults to 90 days. (azurerm_key_vault)docs/design-notes/0001-delegate-storage-data-plane-to-azurite.md: design note capturing the Azurite delegation decision and its rationale, alternatives, and consequences. Status: Implemented.docs/SETUP.md: Storage and Azurite section;AZEMU_AZURITE_ENDPOINTin the env-var table; Azurite port table.
[v0.1.0] - 2026-04-21¶
Added (Phase 5: governance and CI)¶
CONTRIBUTING.mdwith ground rules, dev environment, add-resource walkthrough, test requirements, and PR checklist.CODE_OF_CONDUCT.mdreferencing Contributor Covenant v2.1.SECURITY.mdwith supported versions, private reporting channels (GitHub Security Advisories, email), and 48h acknowledgement SLA.RELEASING.mdwith the full release checklist (pre-release, changelog, tag, goreleaser, post-release verification)..github/CODEOWNERSwith @ZeroDeth as default owner..github/ISSUE_TEMPLATE/bug_report.mdasking for azemu/azurerm/terraform versions, full error output, and/api/unhandledoutput..github/ISSUE_TEMPLATE/feature_request.md..github/PULL_REQUEST_TEMPLATE.mdwith pre-filled checklist..github/workflows/ci.ymlwith lint (golangci-lint, markdownlint), test (go test -race -coverprofile), and build jobs. Triggers on push/PR to main..github/workflows/release.ymltriggering goreleaser on tag push..github/dependabot.ymlfor gomod (weekly), github-actions (weekly), and docker (monthly)..goreleaser.ymlbuilding macOS/Linux/Windows (amd64+arm64) binaries and Docker image toghcr.io/zerodeth/azemu..golangci.ymlexcludingfmt.Fprint*and(io.ReadCloser).Closefrom errcheck..markdownlint-cli2.jsoncdisabling MD013, MD033, MD041.docs/PARITY.mdProof column linking every Full row to its test file.docs/ARCHITECTURE.mdmermaid request-flow diagram.
Added (Phase 1-4)¶
.claude/agents/*.md— five frontmatter-driven subagent definitions (arm-resource-implementer,test-writer,code-reviewer,terraform-compatibility-debugger,docs-writer). Claude Code auto-delegates when a task description matches; previously these roles lived as prose recipes indocs/SUBAGENTS.mdand had to be hand-copied into Task tool invocations. Per https://code.claude.com/docs/en/sub-agents..claude/skills/*/SKILL.md— four slash-invokable playbooks (/add-resource,/modify-store,/validate-terraform,/before-commit).before-commitcarriesdisable-model-invocation: trueso Claude never auto-runs the full validation sequence. Per https://code.claude.com/docs/en/skills..gitignorenegations for.claude/agents/and.claude/skills/so the new directories are version-controlled alongside the existing.claude/rules/exception.
Changed¶
docs/SUBAGENTS.mdrenamed todocs/ORCHESTRATION.mdand trimmed to the three multi-agent composition patterns (parallel resource implementation, test-then-fix, coverage push). The five role definitions moved to.claude/agents/*.md.docs/CHECKLISTS.mdreplaced with an 18-line redirect table pointing at the new skills. Existing content moved verbatim into the four skill files.AGENTS.md"Subagents and orchestration" section rewritten to document the new.claude/agents/and.claude/skills/directories and the/before-commit//validate-terraform//add-resource//modify-storeslash invocations. Project-files table updated to match..claude/rules/arm-handlers.md,.claude/rules/tests.md,.claude/rules/docs.md, anddocs/CONVENTIONS.mdupdated to reference the new skill paths instead of the olddocs/CHECKLISTS.mdanddocs/SUBAGENTS.mdlocations.
Fixed¶
CLAUDE.mdreferenced a machine-local auto-memory file (~/.claude/projects/.../memory/feedback_claude_md_steering.md) that only existed on the maintainer's machine. Anthropic's own memory docs state auto memory is machine-local and not shared across machines, so any contributor cloning the repo on a fresh machine would hit a dangling reference. The Anthropic source quotes and refactor history now live inline in theCLAUDE.mdHTML maintainer comment, which is stripped before context injection (zero session-token cost) and travels with the repo. Per https://code.claude.com/docs/en/memory.
Added¶
Dockerfilewith a multi-stage Go build, alpine runtime,wgetfor healthchecks,VOLUME /azemu, env defaults forAZEMU_CERT_PATHandAZEMU_METADATA_HOST, andEXPOSE 4566 4567 4568.docker-compose.ymlfor single-node local use: exposes4566/4567/4568, bind-mounts./.azemu:/azemu, healthcheck viawget http://localhost:4568/health.GET /healthplain-HTTP endpoint on a configurableHealthPort(default4568). Returns{"status":"ok","version":"...","uptime_seconds":N}. No TLS and no middleware so container probes stay boring.- Startup banner to stderr with version, ports, and cert path. Linker-
overridable
var Version = "dev"via-ldflags "-X main.Version=$(VERSION)". --helpand--versionstdlib-flaghandling incmd/azemu/main.gowith an env-var table and port layout.scripts/aztfwrapper that detects a running azemu viadocker compose ps, starts it if absent, exportsSSL_CERT_FILEand theARM_*variables, and execsterraform "$@". Shellcheck-clean.scripts/trust-cert.shhelper (macOSsecurity add-trusted-cert, Linuxupdate-ca-certificates). Optional; the default path usesSSL_CERT_FILEinstead.examples/terraform/bootstrap config (RG + VNet + Subnet acrossprovider.tf/variables.tf/outputs.tf) plusexamples/terraform/main.tftest.hclnative Terraform 1.6+ test with onerun "full_lifecycle"block.flake.nixfor Nix users outside flox:buildGoModuleforcmd/azemuanddevShells.defaultwith go + terraform.- Makefile targets:
tf-test,coverage,docker-compose,docker-compose-down, plus-ldflags "-X main.Version=..."onbuild. docs/SETUP.mdDocker quick-start section and an expanded make-targets table.- File-backed state store (
internal/store/file.go).FileStorewrapsMemoryStorewith write-through persistence via atomictmp + rename. --persistCLI flag (alsoAZEMU_PERSIST_PATHenv var) that activatesFileStore.--importloads state at startup;--exportdumps current state to a file and exits0.GET /api/state/exportreturns full state as JSON.POST /api/state/importreplaces current state from the request body.POST /api/state/resetclears all resources.Store.Reset()added to the interface so memory + file stores both implement it.- 10
internal/store/file_test.gotests covering write-through, reload, timestamps, delete, reset, tmp cleanup, missing file, corrupt file, import, and concurrent access.
Changed¶
- Cert bundle file mode in
internal/auth/tls.gorelaxed from0600to0644so Docker bind-mounts are readable by the host user when the container writes the file. pkg/config/config.gogrew aHealthPortfield (default4568) next toHTTPPort/HTTPSPort..gitignorenow coverscoverage.html.- Pre-Phase-4 hardening (7 critical + 9 high issues from Go review):
all
store.Putcall sites (RG, VNet, Subnet) now surface errors and return500with Azure error format on failure; store copy semantics fixed so callers cannot mutate stored resources;writeJSONswitched to buffer-first so a failed encode no longer produces a half-written body; auth errors propagate instead of being swallowed; middleware singletons removed.
Fixed¶
azureTimestampdead code ininternal/arm/router.go(0% coverage, never called) deleted during the Phase 2 closeout batch.putResourceGroupnow rejects empty or whitespace-onlylocationwith400 InvalidRequestContent, matching the vnet/subnet pattern. Pinned byTestRG_PUT_MissingLocation_Returns400andTestRG_PUT_WhitespaceOnlyLocation_Returns400.headSubnet/deleteSubnet/writeVNetListcoverage gaps backfilled to 100% viaTestSubnet_HEAD_NotFound_Returns404_EmptyBody,TestSubnet_DELETE_NotFound_Returns404, andTestVNet_LIST_ByRG_FiltersOutSubnets.internal/armpackage coverage climbed from 90.7% to 92.6%.
Added¶
- Virtual Networks (
Microsoft.Network/virtualNetworks) ARM CRUD + HEAD with cascade-delete and child-subnet embedding on GET. - Subnets (
Microsoft.Network/virtualNetworks/subnets) ARM CRUD + HEAD with parent-vnet existence check (returns404 ParentResourceNotFound). internal/middleware/pathcase.goNormalizePathmiddleware that lowercases known ARM literal segments (case-insensitive) and collapses double slashes. Wired into the router beforeRequireAPIVersionso real azurerm camelCase paths reach lowercase chi routes.listResourceGroupResourceshandler (GET /subscriptions/.../resourceGroups/{rg}/resources) returning{"value": []}soterraform destroycan poll an empty RG without hitting/api/unhandledand surfacing a misleading "internal-error".AZEMU_CERT_PATHconfig option andauth.LoadOrGenerateSelfSignedTLS: when set, azemu loads or generates a persistent PEM bundle (cert + EC private key, mode0600) so contributors trust the self-signed cert in their keychain once and can restart the binary freely.internal/arm/testutil_test.goshared test helpers (newTestServer,withAPIVersion,httpPut/httpGet/httpHead/httpDelete,decodeJSON).- 4 metadata regression tests pinning canonical field/suffix names, the
IsAzureStackclassifier conditions, and the all-HTTPS data plane invariant. - 8 path-normalization regression tests covering the exact azurerm camelCase path strings, OAuth path passthrough, and double-slash collapse.
- 4 RG
resourceslisting tests (empty, populated, RG self-exclusion, OData). .flox/env/manifest.tomlpinning Go, Terraform^1.14, just, jq, shellcheck, tflint, pre-commit. Profile definesazemu-start/azemu-stop/azemu-statusandtf-init/tf-plan/tf-apply/tf-destroyaliases. Activation hook installs the project pre-commit hook on first run..pre-commit-config.yamlwith trailing-whitespace, end-of-file-fixer, check-yaml/json, mixed-line-ending, no-commit-to-branch=main, tekwizely/pre-commit-golanggo-fmt/go-vet-repo-mod/go-build-repo-mod,golangci-lintv1.62.2 andmarkdownlint-cliv0.42.0.docs/SETUP.mdanddocs/TROUBLESHOOTING.mdcovering provider redirection, cert trust on macOS/Linux, and the IPv6/localhostresolution gotcha.docs/ARCHITECTURE.md,docs/CONVENTIONS.md,docs/CHECKLISTS.md, anddocs/SUBAGENTS.mdextracted from the previous monolithicCLAUDE.md..claude/rules/arm-handlers.md,.claude/rules/go-style.md,.claude/rules/tests.md, and.claude/rules/docs.md— path-scoped rule files that load only when Claude Code is editing matching files, per the mechanism documented at https://code.claude.com/docs/en/memory.
Changed¶
CLAUDE.mdrefactored from 643 lines to 43 lines to match Anthropic's published guidance ("target under 200 lines per CLAUDE.md file"). The file is now a thin wrapper that importsAGENTS.mdvia the@directive and adds a handful of Claude-Code-specific overrides. Code blocks, ARM contract tables, auth fidelity rules, per-package coverage targets, and workflow checklists moved todocs/CONVENTIONS.md,docs/CHECKLISTS.md, and the.claude/rules/*.mdpath-scoped files.AGENTS.mdrefactored from 215 lines to 116 lines and promoted to the primary "README for agents" (https://agents.md cross-vendor spec). Subagent role definitions and orchestration patterns moved todocs/SUBAGENTS.md.AGENTS.mdnow contains project identity, build/test commands, convention pointers, branch discipline, and safety rules.-
Per-session steering context reduced from ~643 lines (just
CLAUDE.md) to 159 lines (CLAUDE.md+ importedAGENTS.md), a 75% reduction in the context tokens consumed at session start. -
internal/metadata/service.gorewritten against the canonical Azure schema fromhttps://management.azure.com/metadata/endpoints?api-version=2022-09-01. Field names now match real Azure verbatim (portal,graph,appInsightsResourceId,attestationResourceId,synapseAnalyticsResourceId,logAnalyticsResourceId,ossrDbmsResourceId,suffixes.storage,suffixes.keyVaultDns,suffixes.storageSyncEndpointSuffix, ...) sogo-azure-sdkcan build per-service authorizers without falling through to the Azure Stack rejection path. - ARM port
:4566now serves HTTPS (was HTTP) so theazurermprovider does not classify the environment as Azure Stack via theresourceManagerURL scheme check. Both ports share the same self-signed certificate. cmd/azemu/main.gonow starts both servers with a shared TLS config and wiresNormalizePathbeforeRequireAPIVersion. Cert lifecycle messages distinguish "generated and persisted" vs "loaded from existing bundle".
Fixed¶
- M1: Azure Stack rejection caused by
dataPlaneURLs declared ashttp://. Switched tohttps://and pinned byTestMetadata_DataPlaneFieldsAreHTTPS. - M2: Azure Stack rejection caused by
authentication.tenantbeing a UUID; theIsAzureStackclassifier requires the literal string"common". - M3: Storage authorizer build failure caused by hand-rolled metadata field names that did not match Azure's canonical schema.
- M4: chi v5 case-sensitivity mismatch where azurerm sent camelCase
resourceGroupsand azemu's routes were registered as lowercaseresourcegroups. Resolved structurally byNormalizePath. - M5:
terraform destroypolling loop misreported a 501 from the missing RG resources list endpoint as a generic internal-error. .gitignoreazemupattern was matchingcmd/azemu/as a directory wildcard, socmd/azemu/main.gohad never been tracked. Removed the bare pattern; the file is now in version control.
[0.0.1] - 2026-04-09¶
Added¶
- Project scaffold: dual HTTP/HTTPS server, chi routing, zerolog logging
- Metadata service (
/metadata/endpoints) for azurerm provider redirection - Mock OAuth2 token endpoint with RS256 JWT signing
- OIDC discovery and JWKS endpoints
- ARM facade: subscriptions, provider registration, resource group CRUD
- Azure-compatible middleware: response headers, api-version enforcement
- In-memory state store with export/import
- Self-signed TLS certificate generation (ECDSA P-256)
- Dockerfile (multi-stage Go build)
- Makefile with build, run, test, docker, smoke targets
- Example Terraform config (
test/terraform/main.tf) - CLAUDE.md, AGENTS.md, TASKS.md for AI agent orchestration
- docs/PARITY.md resource compatibility matrix