agentleFS
floci-iofloci-io/floci-az/AGENTS.md

floci-az

AGENTS.md814 starsChanged 21 days agoMITView on GitHub

Guidance for AI coding agents working in the floci-az repository. This file defines repository-specific operating rules for autonomous or semi-autonomous coding agents. Follow these instructions unless a maintainer explicitly tells you otherwise. floci-az is a Java-based local Azure emulator built on Quarkus. Its goal is full Azure SDK and Azure CLI compatibility through real Azure wire protocols, not convenience APIs or simplified abstractions. floci-az is the Azure counterpart of Floci (the AWS emulator) and floci-gcp. They share design patterns and…

  • Reads credentials

We split this AGENTS.md into the rules it gives an agent

65 rules. About 0 are common, 33 are distinctive, and 32 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈33

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 32

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When making changes, follow these priorities:

    line 29
  2. Distinctive:

    Prefer correctness over convenience

    line 34
  3. Distinctive:

    Keep changes narrow and testable

    line 35
  4. Distinctive:

    Do not introduce custom endpoint shapes

    line 39
  5. Distinctive:

    Do not change request or response formats for convenience

    line 40
  6. Distinctive:

    Do not perform broad refactors unless the task explicitly requires them

    line 41
  7. Very specific:

    Keep behavior aligned with Azure SDK expectations and existing floci-az conventions

    line 42
  8. Very specific:

    Use XmlBuilder for XML responses (supports start, startAttr, selfClose, elem, raw, end)

    line 144
  9. Very specific:

    Use XmlParser for XML parsing.

    line 145
  10. Very specific:

    Use XmlUtils for Jackson-based serialisation of annotated model classes

    line 146
  11. Very specific:

    Always use StorageFactory to obtain a StorageBackend

    line 158
  12. Distinctive:

    Do not instantiate storage implementations directly inside handlers

    line 159
  13. Distinctive:

    When adding storage-related behavior:

    line 163
  14. Very specific:

    Update EmulatorConfig (add ServiceStorageConfig accessor)

    line 165
  15. Very specific:

    Update ServicesStorageConfig in EmulatorConfig

    line 166
  16. Very specific:

    Add storage mode to BannerLogger.getStorageMode()

    line 169
  17. Very specific:

    Add interface method to EmulatorConfig (inner interface or new @WithDefault)

    line 179
  18. Very specific:

    Add to ServicesConfig if service-scoped

    line 180
  19. Very specific:

    Add to main application.yml

    line 181
  20. Very specific:

    Update BannerLogger if user-visible

    line 182
  21. Distinctive:

    Update documentation if user-facing

    line 183
  22. Very specific:

    Follow FLOCI_AZ_* environment variable conventions (SmallRye converts .

    line 184
  23. Very specific:

    Always call lifecycleManager.removeIfExists(containerName) before creating to clean up stale containers

    line 202
  24. Very specific:

    Use ContainerBuilder + ContainerLifecycleManager.

    line 203
  25. Distinctive:

    Always validate new implementations against the relevant SDK test suite

    line 259
  26. Distinctive:

    Prefer SDK clients over raw HTTP for all validation

    line 260
  27. Very specific:

    Use make test-<service> to run a specific suite

    line 261
  28. Very specific:

    Use make compat-docker to run all suites against a running container

    line 262
  29. Distinctive:

    If a change affects request parsing, response shape, error handling, persistence semantics, URL generation, or service enablement:

    line 363
  30. Distinctive:

    Add or update automated tests

    line 365
  31. Very specific:

    Prefer SDK-based verification in compatibility-tests/

    line 366
  32. Distinctive:

    Check behavior across all relevant SDK languages

    line 367
  33. Distinctive:

    Document intentional deviations clearly

    line 368
  34. Very specific:

    Use AzureErrorResponse helper for XML-format errors (Storage services)

    line 375
  35. Distinctive:

    When adding a new HTTP-based service:

    line 382
  36. Very specific:

    Add *Handler.java implementing AzureServiceHandler

    line 385
  37. Very specific:

    Add config interface to EmulatorConfig.ServicesConfig

    line 402
  38. Very specific:

    Add YAML block to application.yml

    line 403
  39. Distinctive:

    Add storage wiring if needed

    line 404
  40. Very specific:

    Update docs (docs/services/<svc>.md, docs/services/index.md, mkdocs.yml, README.md)

    line 407
  41. Distinctive:

    When adding a sidecar-based service, additionally:

    line 409
  42. Distinctive:

    When touching a class that still uses field injection, migrate it to constructor injection if the change is small.

    line 424
  43. Very specific:

    Never leave a catch block empty.

    line 429
  44. Distinctive:

    Keep methods small and focused.

    line 434
  45. Distinctive:

    Prefer self-explanatory code over comments.

    line 435
  46. Distinctive:

    Always use braces in conditionals

    line 436
  47. Distinctive:

    Follow existing project patterns before introducing new abstractions

    line 437
  48. Distinctive:

    Use modern Java features when they improve clarity

    line 438
  49. Distinctive:

    Use Java records for simple data carriers.

    line 439
  50. Very specific:

    No wildcard imports in src/main, including static ones.

    line 458
  51. Very specific:

    Import order: non-java/javax imports alphabetically, then java.* and javax.* last.

    line 460
  52. Distinctive:

    No em-dashes anywhere, in any content.

    line 499
  53. Very specific:

    Use JBoss Logging (Logger.getLogger(MyClass.class)) in src/main/java

    line 503
  54. Very specific:

    Use LOG.infov(...) / LOG.debugv(...) for parameterised messages

    line 508
  55. Distinctive:

    Keep logs structured; avoid noisy logs in hot paths

    line 509
  56. Distinctive:

    Keep changes focused; avoid unrelated refactors

    line 516
  57. Distinctive:

    Update docs when functionality changes

    line 518
  58. Very specific:

    Do not add Co-Authored-By trailers for AI tools in commit messages.

    line 529
  59. Very specific:

    Use the workflow's dry-run input to preview the next version and notes without tagging.

    line 539
  60. Distinctive:

    When in doubt about Azure wire protocol behavior, refer to real sources:

    line 546
  61. Distinctive:

    Do not invent protocol behavior.

    line 552
  62. Distinctive:

    Do not introduce non-Azure endpoint shapes

    line 558
  63. Very specific:

    Do not bypass StorageFactory for storage operations

    line 559
  64. Very specific:

    Do not call dockerClient directly in new service code.

    line 560
  65. Distinctive:

    Do not make sidecar startup failures fatal (they would break unit tests and environments without Docker)

    line 561

≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

Guidance for AI coding agents working in the floci-az repository.

This file defines repository-specific operating rules for autonomous or semi-autonomous coding agents. Follow these instructions unless a maintainer explicitly tells you otherwise.


Project Overview

floci-az is a Java-based local Azure emulator built on Quarkus.

Its goal is full Azure SDK and Azure CLI compatibility through real Azure wire protocols, not convenience APIs or simplified abstractions.

floci-az is the Azure counterpart of Floci (the AWS emulator) and floci-gcp. They share design patterns and Docker infrastructure, but are independent repositories.

  • Port: 4577 (all HTTP services share this single port)
  • AMQP port: 5672 (Event Hubs, managed by Artemis sidecar)
  • Kafka port: 9093 (Event Hubs Kafka, managed by Redpanda sidecar, opt-in)
  • Stack:
    • Java 25
    • Quarkus 3.x
    • JUnit 5 + RestAssured
    • Jackson
    • docker-java for sidecar container management

First Principles

When making changes, follow these priorities:

  1. Preserve Azure protocol compatibility
  2. Match Azure SDK and CLI behavior
  3. Reuse existing floci-az patterns
  4. Prefer correctness over convenience
  5. Keep changes narrow and testable

Critical rules:

  • Do not introduce custom endpoint shapes
  • Do not change request or response formats for convenience
  • Do not perform broad refactors unless the task explicitly requires them
  • Keep behavior aligned with Azure SDK expectations and existing floci-az conventions

Architecture

floci-az follows a layered design:

  • AzureRoutingFilter: pre-matching JAX-RS filter; extracts account name and service type from the request path; dispatches to the correct AzureServiceHandler
  • AzureServiceHandler: interface implemented by each service (getServiceType(), handle())
  • AzureServiceRegistry: CDI registry of all handlers; checks isEnabled() per service
  • Service Handler: parses Azure protocol input, contains business logic, produces Azure-compatible responses
  • StorageBackend: pluggable persistence (memory / persistent / hybrid / wal)

Core Infrastructure

  • EmulatorConfig: SmallRye @ConfigMapping; prefix floci-az
  • AzureRoutingFilter: assembles its host, account-suffix and ARM-provider dispatch tables at @PostConstruct from every handler's routes(); no per-service suffix list lives in the filter
  • ServiceRoutes: the routing declaration a handler returns from routes() (account suffixes such as -queue, host suffixes such as .blob.core.windows.net, ARM providers such as Microsoft.App)
  • AzureServiceRegistry: handler discovery + isEnabled(), which delegates to handler.enabled(serviceType)
  • BannerLogger: startup banner listing all enabled services
  • StorageBackend + StorageFactory: pluggable storage
  • XmlBuilder: fluent XML builder with attribute support (startAttr, selfClose)
  • XmlParser: StAX-based XML parser (no extra dependencies)
  • XmlUtils: Jackson-based XML serialisation for structured models

Sidecar Container Infrastructure (core/docker/)

Used by services that delegate to a managed Docker container:

  • ContainerSpec: immutable container descriptor (record)
  • ContainerBuilder: fluent ContainerSpec builder (network, ports, mounts, log rotation, DNS)
  • ContainerLifecycleManager: create/start/stop/remove containers; volume management; endpoint resolution
  • ImageCacheService: pull-once-per-image with registry credential support
  • PortAllocator: thread-safe free TCP port allocation
  • ContainerDetector: detects whether floci-az is running inside Docker
  • CurrentContainerNetworkResolver: resolves which Docker network floci-az is on
  • DockerHostResolver: resolves the correct host for child containers to reach floci-az
  • DockerClientProducer: CDI producer for DockerClient

Package Layout

io.floci.az.config           : EmulatorConfig
io.floci.az.core             : routing, registry, banner, XML utils, auth, DNS
io.floci.az.core.auth        : auth pipeline, verifiers
io.floci.az.core.docker      : container infrastructure
io.floci.az.core.storage     : storage backends
io.floci.az.core.ui          : web console sidecar (/_floci/ui), Console Contract v2 environment
io.floci.az.services.<svc>   : per-service handlers

Typical service structure:

services/<svc>/
  *Handler.java       : implements AzureServiceHandler
  *Models.java        : domain objects (optional)

Sidecar-based services additionally have:

services/<svc>/
  *ContainerManager.java   : @ApplicationScoped, @PostConstruct/@PreDestroy lifecycle
  *Manager.java            : manages one sidecar (pull, start, health-wait, stop)
  *ConfigGenerator.java    : generates sidecar config file from EmulatorConfig

Rule: copy an existing service pattern before introducing a new one.


Azure Protocol Rules

ServiceProtocolPath SuffixNotes
Blob StorageAzure Storage REST/{account}/XML responses, Shared Key auth
Queue StorageAzure Storage REST/{account}-queue/XML responses
Table StorageAzure Storage REST/{account}-table/JSON / OData
Azure FunctionsHTTP REST/{account}-functions/ZIP deploy, HTTP trigger invoke
App ConfigurationAzure App Config REST/{account}-appconfig/JSON, ETags, labels
Key VaultAzure Key Vault REST/{account}-keyvault/JSON, Bearer auth challenge
Event HubsAMQP 1.0 (Artemis)port 5672Direct sidecar, not proxied through 4577
Event Hubs KafkaKafka (Redpanda)port 9093Optional, direct sidecar

Key Vault auth challenge

The azure-keyvault SDK uses ChallengeAuthPolicy: it sends a probe request without a body to elicit a 401 WWW-Authenticate response. The handler must return 401 when no Authorization header is present. Always include verify_challenge_resource=False in SDK test clients (localhost can never match .vault.azure.net).

Event Hubs

Event Hubs does not use the HTTP port for data-plane traffic. All SDK send/receive goes over AMQP 1.0 directly to the Artemis sidecar on port 5672. The /{account}-eventhub/ path at port 4577 is admin/health only.


XML / JSON Rules

  • Use XmlBuilder for XML responses (supports start, startAttr, selfClose, elem, raw, end)
  • Use XmlParser for XML parsing; do not use regex or string manipulation for XML
  • Use XmlUtils for Jackson-based serialisation of annotated model classes
  • JSON errors must follow Azure error structures ({"error":{"code":"...","message":"..."}})
  • Types returned from handlers must remain reflection-safe for GraalVM native image

Storage Rules

Supported storage modes: memory, persistent, hybrid, wal

Rules:

  • Always use StorageFactory to obtain a StorageBackend
  • Do not instantiate storage implementations directly inside handlers
  • Respect lifecycle hooks for load and flush behavior
  • Event Hubs does not use StorageBackend: it delegates entirely to the Artemis sidecar

When adding storage-related behavior:

  1. Update EmulatorConfig (add ServiceStorageConfig accessor)
  2. Update ServicesStorageConfig in EmulatorConfig
  3. Update main application.yml
  4. Wire through StorageFactory
  5. Add storage mode to BannerLogger.getStorageMode()

Configuration Rules

Configuration lives under floci-az.* and maps to EmulatorConfig via SmallRye @ConfigMapping.

When adding config:

  1. Add interface method to EmulatorConfig (inner interface or new @WithDefault)
  2. Add to ServicesConfig if service-scoped
  3. Add to main application.yml
  4. Update BannerLogger if user-visible
  5. Update documentation if user-facing
  6. Follow FLOCI_AZ_* environment variable conventions (SmallRye converts . → _ and lowercases)

Critical areas:

  • base-url / hostname: affects URLs returned in API responses
  • port: default 4577
  • storage.mode: global default; per-service override via storage.services.<svc>.mode
  • services.docker-network: shared Docker network for sidecar containers
  • docker.docker-host: Docker daemon socket

Sidecar Container Rules

Services that use sidecar containers (Event Hubs, Service Bus, SQL, PostgreSQL, Redis, ACR, AKS, ...) follow this pattern:

  1. *ContainerManager observes StartupEvent and calls *Manager.start(); @PreDestroy calls stop()
  2. Startup failures must be non-fatal: log an error and return; do not throw and crash the app
  3. Always call lifecycleManager.removeIfExists(containerName) before creating to clean up stale containers
  4. Use ContainerBuilder + ContainerLifecycleManager. Do not call dockerClient directly in new code
  5. Health-wait after start: TCP poll for AMQP/port-based services; HTTP poll for services with a /ready endpoint
  6. Config files generated for sidecars (e.g. broker.xml) must use XmlBuilder for XML, not string concatenation

Build & Run

./mvnw quarkus:dev
./mvnw test
./mvnw clean package
./mvnw clean package -DskipTests

Focused tests

./mvnw test -Dtest=HealthTest
./mvnw test -Dtest=BlobServiceTest#testUploadBlob

Manual testing

make run        # start emulator in background (for manual SDK testing)
make test       # run unit tests + all compat tests (starts its own Docker emulator)
make stop

Compatibility Test Suite

compatibility-tests/
  sdk-test-python/      : Azure SDK for Python (pytest): blob/queue/table, cosmos, vm, redis, acr,
                          managed identity, plus appconfig/ and keyvault/ sub-suites
  sdk-test-java/        : Azure SDK for Java (BOM; the primary compat target): blob/queue/table,
                          cosmos + all engines, functions, keyvault, appconfig, eventhub,
                          servicebus, eventgrid, apim, sql, postgres, vm, datalake,
                          managed identity
  sdk-test-node/        : Azure SDK for JS (jest): blob, queue, table, cosmos, appconfig,
                          keyvault, eventhub, servicebus, managed identity
  sdk-test-dotnet/      : Azure SDK for .NET (TUnit): blob service SAS, cosmos (+ bracket properties,
                          point partition, transactional batch), keyvault (+ certificates, timestamps),
                          servicebus (+ lock renewal, metadata, peek-lock), signalr. The only suite
                          covering SignalR
  sdk-test-cpp/         : Azure SDK for C++ (GoogleTest via vcpkg): blob, queue. Built from source, so ~6 min cold
  compat-terraform/     : hashicorp/azurerm Terraform provider (BATS)
  compat-opentofu/      : OpenTofu with the azurerm provider (BATS)
  compat-azcli/         : real `az` CLI against a custom registered cloud (BATS)

Guidelines:

  • Always validate new implementations against the relevant SDK test suite
  • Prefer SDK clients over raw HTTP for all validation
  • Use make test-<service> to run a specific suite
  • Use make compat-docker to run all suites against a running container
  • compat-terraform and compat-opentofu must declare the same resource blocks in main.tf. CI checks it with .github/ci/compat-iac-drift-check.py; add a new resource to both suites, or list it in the matching .github/ci/compat-iac-allowlist-*.txt with the reason

Suites that do not run in CI

Seven classes in sdk-test-java skip themselves unless an environment variable enables the thing they test, and no workflow sets any of them, so a green sdk-test-java is not evidence for these:

ClassGated onRun it locally with
Cosmos{Mongo,Postgres,Cassandra,Gremlin,Table,NoSql}EngineCompatibilityTestFLOCI_AZ_SERVICES_COSMOS_ENGINES_<ENGINE>_ENABLEDmake test-cosmos-<engine>, or make test-cosmos-all
SqlCompatibilityTestFLOCI_AZ_SERVICES_SQL_ACCEPT_EULA and FLOCI_AZ_SERVICES_SQL_DATA_PLANE_PROVIDER=managedmake test-sql

Four of the six Cosmos engines (mongo, postgres, cassandra, gremlin) need a container each, which is why the set is opt-in rather than part of the matrix; table and nosql are embedded and run in-process, so they need no Docker and are cheap to run locally. The consequence either way is that a regression in a Cosmos engine or in the SQL data plane merges green. When you change either, run the suite locally and say so in the PR.

A suite reporting Tests run: 0 is a self-skip, never a pass. CI and the Makefile's Docker suite targets enforce this with .github/ci/compat-test-count-check.py, which fails a suite whose JUnit reports show no executed test (CI also applies a per-suite floor, min_tests in the matrix).

Keeping the Makefile and CI in sync

The Makefile and the CI matrix (.github/workflows/compatibility.yml) both run the SDK test containers against floci-az and must pass the same environment variables per suite.

Rule: a suite's env vars must be identical across every docker run for that suite in the Makefile and the corresponding matrix entry in .github/workflows/compatibility.yml, on both the suite-container axis (SUITE_ENV_* ↔ extra_env) and the emulator-container axis (the 6th COMPAT_SESSION argument ↔ emulator_env). Define them once (a shared variable), never inline per call site.

Suite-internal defaults (FLOCI_AZ_ENDPOINT, EVENTHUB_*, JEST_JUNIT_*) are baked as ENV in each suite's Dockerfile with the compat-network values. They do NOT belong in the Makefile or extra_env. Only network-topology overrides (sidecar hostnames/ports, emulator-endpoint redirects) go there.

There are two axes, and both must stay in sync. The suite container's env comes from SUITE_ENV_<SUITE> in the Makefile and extra_env in the matrix. The emulator container's env comes from the 6th COMPAT_SESSION argument in the Makefile and emulator_env in the matrix; it is easy to miss, because a var set only there does not appear in any SUITE_ENV_* variable.

One exception on the emulator axis: compat-docker. It starts a single shared emulator with JAVA_EMULATOR_ENV (so including FLOCI_AZ_SERVICES_SERVICE_BUS_LOCK_DURATION_SECONDS=5 and FLOCI_AZ_SERVICES_AKS_MOCKED=true) and runs every suite against it. The python, node and dotnet suites therefore see a longer lock duration there than under their own test-*-compat targets or in CI, where they get SERVICEBUS_EMULATOR_ENV alone. The table below is the per-suite contract that CI and the individual targets honour; compat-docker is a convenience runner that deliberately shares one emulator, so a Service Bus timing behaviour that only reproduces under it is worth suspecting there first.

Suite container (SUITE_ENV_* ↔ extra_env):

SuiteMakefileCI (compatibility.yml extra_env)
sdk-test-java--add-host devstoreaccount1.dfs.core.windows.net:127.0.0.1✓
sdk-test-java-e FLOCI_AZ_ABFS_LOOPBACK=true✓
sdk-test-java-e SERVICEBUS_HOST=floci-az-servicebus-default✓
sdk-test-java-e SERVICEBUS_AMQPS_PORT=5671✓
sdk-test-java-e SERVICEBUS_NAMESPACE=default✓
sdk-test-java-e AZURE_POD_IDENTITY_AUTHORITY_HOST=http://floci-az:4577✓
sdk-test-dotnet-e SERVICEBUS_HOST=floci-az-servicebus-default✓
sdk-test-dotnet-e SERVICEBUS_AMQP_PORT=5672✓
sdk-test-python-e AZURE_POD_IDENTITY_AUTHORITY_HOST=http://floci-az:4577✓
sdk-test-python-e SERVICEBUS_HOST=floci-az-servicebus-default✓
sdk-test-python-e SERVICEBUS_AMQP_PORT=5672✓
sdk-test-python-e SERVICEBUS_NAMESPACE=default✓
sdk-test-node-e AZURE_POD_IDENTITY_AUTHORITY_HOST=http://floci-az:4577✓
sdk-test-node-e SERVICEBUS_HOST=floci-az-servicebus-default✓
sdk-test-node-e SERVICEBUS_AMQP_PORT=5672✓
sdk-test-node-e SERVICEBUS_NAMESPACE=default✓
sdk-test-cppnone, only the shared FLOCI_AZ_ENDPOINT✓ (no extra_env)

Emulator container (6th COMPAT_SESSION argument ↔ emulator_env):

SuiteMakefileCI (compatibility.yml emulator_env)
sdk-test-java-e FLOCI_AZ_SERVICES_SERVICE_BUS_MOCKED=false (JAVA_EMULATOR_ENV)✓
sdk-test-java-e FLOCI_AZ_SERVICES_SERVICE_BUS_LOCK_DURATION_SECONDS=5✓
sdk-test-java-e FLOCI_AZ_SERVICES_AKS_MOCKED=true✓
sdk-test-dotnet-e FLOCI_AZ_SERVICES_SERVICE_BUS_MOCKED=false (SERVICEBUS_EMULATOR_ENV)✓
sdk-test-python-e FLOCI_AZ_SERVICES_SERVICE_BUS_MOCKED=false (SERVICEBUS_EMULATOR_ENV)✓
sdk-test-node-e FLOCI_AZ_SERVICES_SERVICE_BUS_MOCKED=false (SERVICEBUS_EMULATOR_ENV)✓
every suite, parity legs only-e FLOCI_AZ_PARITY=azure (PARITY_EMULATOR_ENV, from PARITY=azure)✓ (matrix.parity, scheduled and manual runs only)

The parity legs run every suite against an emulator started at floci-az.parity=azure. In CI they are the parity matrix axis, which is empty on pull requests and azure on the scheduled and manually dispatched runs, so an opt-in behaviour cannot fail a PR. Locally, add PARITY=azure to any Docker compat target (make test-terraform-compat PARITY=azure, make test-azcli PARITY=azure).

The container name follows the pattern floci-az-<service>-<namespace> (e.g. floci-az-servicebus-default). If a new sidecar-based service is added, its container name and port must be added to both places.


Testing Rules

Conventions

  • Unit/integration tests: src/test/java/io/floci/az/
  • Compatibility tests: compatibility-tests/sdk-test-*/
  • Unit tests use @QuarkusTest + RestAssured
  • Sidecar-based services are disabled in unit tests via graceful Docker failure handling (no Docker socket in CI)

When touching protocol behavior

If a change affects request parsing, response shape, error handling, persistence semantics, URL generation, or service enablement:

  1. Add or update automated tests
  2. Prefer SDK-based verification in compatibility-tests/
  3. Check behavior across all relevant SDK languages
  4. Document intentional deviations clearly

Error Handling

  • Handlers return Response with Azure-compatible error JSON: {"error":{"code":"...","message":"..."}}
  • Use AzureErrorResponse helper for XML-format errors (Storage services)
  • Sidecar startup failures must degrade gracefully: log ERROR and skip, never throw from onStart

Service Implementation Pattern

When adding a new HTTP-based service:

  1. Create services/<svc>/ package
  2. Add *Handler.java implementing AzureServiceHandler
  3. Declare the service's routes on the handler: return a ServiceRoutes from routes() naming its account suffix, host suffixes and ARM providers, and add the same literals to the golden RoutingTableAssemblyTest. A service routed by any of those three needs no AzureRoutingFilter edit at all; the filter builds its tables from routes(). A service whose URLs fit none of the three (IMDS, Entra, Graph, the Cosmos root) needs a stage: a method added to the stages list in AzureRoutingFilter and ordered against its neighbours, plus its service type in LITERAL_ROUTE_SERVICE_TYPES. That set is a declaration for the routing-drift test, not a dispatch mechanism: adding to it alone routes nothing
  4. A service that serves ARM resource types declares them in a *ArmSurface bean next to its handler (ArmSurfaceProvider: namespace, lane, and each type's scope and location policy), adds the namespace to NAMESPACES in tools/arm/gen_providers.py, runs make arm-data-sync, and adds its rows to ArmSurfaceGoldenTest. ArmSurfaceInventoryTest fails for a routed namespace with no surface, and the ArmTypeRegistry refuses a type that is not in providers.json
  5. Implement enabled(String serviceType) on the handler, reading config.services().<svc>().enabled(); AzureServiceRegistry.isEnabled() delegates to it, so there is no central switch to edit
  6. Add config interface to EmulatorConfig.ServicesConfig
  7. Add YAML block to application.yml
  8. Add storage wiring if needed
  9. Update BannerLogger
  10. Add compatibility tests
  11. Update docs (docs/services/<svc>.md, docs/services/index.md, mkdocs.yml, README.md)

When adding a sidecar-based service, additionally:

  • Implement *ContainerManager, *Manager, *ConfigGenerator
  • Expose sidecar ports in the Docker Compose example

Code Style

Dependency injection

  • Always prefer constructor injection (@Inject on the constructor) over field injection (@Inject on fields) in new or modified code:
    • dependencies become private final, so the object is immutable and never observed half-constructed
    • required collaborators are explicit in the signature, and plain unit tests can construct the class without a CDI container
    • with Quarkus Arc, normal-scoped beans are client proxies, so reading fields through an injected bean hits the proxy's own field copies, not the contextual instance. Always go through methods on injected beans; constructor injection plus method access avoids this trap entirely
  • When touching a class that still uses field injection, migrate it to constructor injection if the change is small; otherwise stay consistent within the class and flag it

General practices

  • Make fields final wherever possible; avoid static mutable state
  • Never leave a catch block empty. If an exception is intentionally tolerated, log it with enough context to diagnose later. When swallowing really is correct and logging would be noise, name the variable ignored or expected and say in a comment why it is safe. A bare catch (Exception e) {} is never acceptable
  • Validate inputs at the protocol boundary and fail fast with the correct Azure error shape
  • Keep methods small and focused; extract helpers instead of growing switch/if ladders
  • Prefer self-explanatory code over comments; only add a comment when the WHY is non-obvious
  • Always use braces in conditionals
  • Follow existing project patterns before introducing new abstractions
  • Use modern Java features when they improve clarity
  • Use Java records for simple data carriers; model Azure resources as typed domain objects when practical

Types and names

  • Do not use var. Write the explicit type. floci-az reproduces Azure wire contracts, so the concrete type at a call site is usually the thing under review: whether a value is a LinkedHashMap or a Map, an Azure model type or a JDK one, is exactly what a reviewer needs to see. This covers local declarations, enhanced-for, classic for-init, and try-with-resources. The one exception is a record deconstruction pattern (case Node(var left, var right) ->), where naming the component types is pure noise
  • Import the classes you use. Do not write fully-qualified names inline. new ArrayList<>(), never new java.util.ArrayList<>(). The only reason to qualify inline is a genuine name collision inside one file: import the type used more often, qualify the other, and leave a short comment naming the clash. A qualified name inside a string literal is data, not a type reference: Jolokia and JMX operation signatures such as "createQueue(java.lang.String)" must stay fully qualified, or the broker matches no operation

Imports

  • No wildcard imports in src/main, including static ones. Static wildcards stay fine in tests, where Assertions.*, Matchers.* and RestAssured.* are the established idiom
  • Import order: non-java/javax imports alphabetically, then java.* and javax.* last. This is the IntelliJ default layout and what most of the tree already uses

Conventions the codebase already follows

Written down so they stay true. New code should match them without thinking. A handful of files predate them; a violation you find in the tree is a straggler, not a precedent.

  • 4-space indentation, K&R braces. Never indent with a tab
  • JBoss Logging, in a field named LOG, using the parameterised ...v() or ...f() form. No string concatenation in log calls. src/artemis-*/ is the exception and is covered under Logging below
  • No printStackTrace, anywhere. No System.out or System.err in src/main
  • java.time for everything floci-az owns. Calendar and SimpleDateFormat appear nowhere and must not be introduced. A Date survives only at a third-party boundary that forces one, currently the BouncyCastle certificate builders and Proton's AMQP timestamps. Convert at that boundary with Date.from(instant) and keep java.time on floci-az's side of it
  • Constructor injection in src/main, per the section above. Field injection is fine in tests
  • Optional as a return type, and never as a field: there are none, keep it that way
  • Switch expressions over switch statements. Pattern-matching instanceof over cast-after-check
  • Azure-shaped error envelopes for domain errors: AzureErrorResponse for the Storage XML shape, and each service's own helper for its JSON envelope (kvError, AcrErrors, Cosmos errorResponse). There is no single exception type; the envelope belongs to the service's wire contract
  • final on service fields, but not on locals or parameters

Tests

These describe src/test. compatibility-tests is a separate module; follow the module you are in.

  • Name test methods as a camelCase sentence (patchingAnImmutableSystemPropertyIsRejected). testX names appear in a few older tests and are not the pattern to copy
  • JUnit 5 assertions with Hamcrest and RestAssured matchers. AssertJ is not used in src/test; do not introduce it
  • @DisplayName is used in about 40% of test classes, mostly where the sentence carries more than a method name can. Both styles are established: match the file you are in

Documentation Style

  • No em-dashes anywhere, in any content. Use colons, commas, or periods.

Logging

  • Use JBoss Logging (Logger.getLogger(MyClass.class)) in src/main/java
  • Code under src/artemis-patch/, src/artemis-amqp-patch/ and src/artemis-plugin/ is compiled into the Artemis sidecar, not into the emulator JVM, so JBoss Logging is not on its classpath. Follow the tree you are editing: src/artemis-amqp-patch/ uses slf4j, src/artemis-plugin/ uses System.Logger, and src/artemis-patch/ logs nothing today
  • Use LOG.infov(...) / LOG.debugv(...) for parameterised messages
  • Keep logs structured; avoid noisy logs in hot paths
  • Sidecar startup/stop should always log at INFO level

Pull Request Guidelines

  • Keep changes focused; avoid unrelated refactors
  • Preserve behavior unless the task explicitly requires change
  • Update docs when functionality changes
  • Explain missing tests when behavior changed but no automated coverage was added

Conventional commits:

  • feat:: new service or feature
  • fix:: bug fix
  • perf:: performance improvement
  • docs:: documentation only
  • chore:: build, CI, dependencies

Do not add Co-Authored-By trailers for AI tools in commit messages.

Releases:

  • Releases are cut by dispatching the "Release Cut" workflow (.github/workflows/release-cut.yml) from main. semantic-release computes the next version from Conventional Commits, updates pom.xml/CHANGELOG.md, tags, and publishes the GitHub Release; the tag push triggers release.yml.
  • The GH_TOKEN repo secret must be a PAT (fine-grained, contents: write): tags pushed with the default GITHUB_TOKEN do not trigger release.yml.
  • Use the workflow's dry-run input to preview the next version and notes without tagging.

Azure Protocol References

When in doubt about Azure wire protocol behavior, refer to real sources:

  • Azure REST API specs: the authoritative request/response/error contract
  • Azure SDK source code (Java, Python, JS)
  • Microsoft official emulator behavior (Azurite for Storage, the official Event Hubs emulator)

Do not invent protocol behavior. If a spec is ambiguous, test against a real Azure endpoint or the official emulator.


Do Not

  • Do not introduce non-Azure endpoint shapes
  • Do not bypass StorageFactory for storage operations
  • Do not call dockerClient directly in new service code. Use ContainerBuilder + ContainerLifecycleManager
  • Do not make sidecar startup failures fatal (they would break unit tests and environments without Docker)

Download now (ZIP)

  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repositoryOne other file it gives its agents.

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.