Guidance for AI coding agents working in the Floci 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 is a Java-based local AWS emulator built on Quarkus.
Its goal is full AWS SDK and AWS CLI compatibility through real AWS wire protocols, not convenience APIs or simplified abstractions.
Floci acts as an open-source alternative to LocalStack Community.
- Port: 4566
- Stack:
- Java 25
- Quarkus 3.39.2
- JUnit 5
- RestAssured
- Jackson
- Docker integrations for Lambda, RDS, and ElastiCache
When making changes, follow these priorities:
- Preserve AWS protocol compatibility
- Match AWS SDK and CLI behavior
- Reuse existing Floci patterns
- Prefer correctness over convenience
- 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 AWS expectations and existing Floci conventions
Floci follows a layered design:
-
Controller / Handler
- Parses AWS protocol input
- Produces AWS-compatible responses
-
Service
- Contains business logic
- Throws
AwsException
-
Model
- Domain objects
EmulatorConfigServiceRegistryStorageBackend+StorageFactoryAwsJson11ControllerAwsQueryControllerAwsException+AwsExceptionMapperEmulatorLifecycle
io.github.hectorvent.floci.configio.github.hectorvent.floci.core.commonio.github.hectorvent.floci.core.storageio.github.hectorvent.floci.lifecycleio.github.hectorvent.floci.services.<service>
Typical service structure:
services/<svc>/*Controller.java*Service.javamodel/
Rule: Copy an existing service pattern before introducing a new one.
Floci must implement real AWS wire protocols.
| Protocol | Services | Request Format | Response Format | Implementation |
|---|---|---|---|---|
| Query | SQS, SNS, IAM, STS, RDS, ElastiCache, CloudFormation, CloudWatch Metrics | form-encoded POST + Action |
XML | AwsQueryController |
| JSON 1.1 | SSM, EventBridge, CloudWatch Logs, Kinesis, KMS, Cognito, Secrets Manager, ACM | POST + X-Amz-Target |
JSON | AwsJson11Controller |
| REST JSON | Lambda, API Gateway, SES V2 | REST paths | JSON | JAX-RS |
| REST XML | S3 | REST paths | XML | JAX-RS |
| TCP | ElastiCache, RDS | raw protocol | native | proxies |
- CloudWatch Metrics supports both Query and JSON 1.1; handlers must remain aligned
- SQS and SNS may expose multiple compatibility paths; do not let them drift
- Cognito well-known endpoints are OIDC REST JSON endpoints, not AWS management APIs
- Data-plane protocols may use raw TCP sockets
- Management APIs should be validated with AWS SDK clients, not only handcrafted HTTP requests
- Use
XmlBuilderfor XML responses - Use
XmlParserfor XML parsing; do not use regex - Use
AwsNamespacesconstants - JSON errors must follow AWS error structures
- Types returned directly from controllers must remain compatible with native-image reflection requirements
Supported storage modes:
memorypersistenthybridwal
Rules:
- Always use
StorageFactory - Do not instantiate storage implementations directly inside services
- Respect lifecycle hooks for load and flush behavior
Important nuance:
Configuration interfaces may declare fallback defaults, but application.yml defines effective runtime behavior. Treat repository YAML as the source of truth unless a task explicitly changes configuration semantics.
When adding storage-related behavior:
- Update
EmulatorConfig - Update main
application.yml - Update test
application.yml - Wire through
StorageFactory - Verify lifecycle integration
Configuration lives under floci.*.
When adding config:
- Add it to
EmulatorConfig - Add it to main
application.yml - Add it to test
application.ymlif needed - Update documentation if user-facing
- Follow
FLOCI_*environment variable conventions
Critical areas:
base-urlhostname- region and account defaults
- port ranges
- persistence paths
- Docker networking
./mvnw quarkus:dev
./mvnw test
./mvnw clean package
./mvnw clean package -DskipTests
make help lists the Makefile shortcuts. The native binary and image: make native
then make native-image (CI's flags, staged in native/<arch>/), make native-host for a
binary built on this machine with the installed GraalVM, make native-up to run the image,
make compat SUITES="sdk-test-java compat-cdk" for compatibility suites against it.
./mvnw test -Dtest=SsmIntegrationTest
./mvnw test -Dtest=SsmIntegrationTest#putParameter
Compatibility test suite: ./compatibility-tests/
Guidelines:
- Prefer AWS SDK clients over raw HTTP for management-plane validation
- Use this suite when changes may affect real SDK behavior
- Unit tests:
*ServiceTest.java - Integration tests:
*IntegrationTest.java - Prefer package-private constructors for testability
- Integration tests may use ordered execution when stateful behavior requires it
- Test any behavior affecting AWS compatibility
- Do not rely only on manual HTTP testing
- Prefer SDK-based validation where possible
If a change affects request parsing, response shape, error handling, persistence semantics, URL generation, or service enablement:
- Add or update automated tests
- Prefer SDK-based verification where possible
- Check compatibility across alternate protocol paths
- Document intentional deviations clearly
- Services should throw
AwsException - Query and REST XML flows should use
AwsExceptionMapper - JSON 1.1 flows should return structured AWS error responses where required
- Controller return types must remain reflection-safe
When adding functionality:
- Identify the AWS protocol
- Reuse an existing service pattern
- Keep controllers thin
- Use
AwsExceptionfor domain errors - Reuse shared utilities
- Update config, storage, docs, and tests together
- Validate behavior against AWS SDK expectations
- Create a package under
services/<svc>/with a Controller, a Service, andmodel/ - Add a
<Svc>ServiceConfiginterface and its accessor onServicesConfiginEmulatorConfig - Add one
descriptor(...)entry inResolvedServiceCatalog. This is the registration point;ServiceRegistryonly reads the catalog and has no registration API - Add
floci.services.<key>.enabledto bothsrc/main/resources/application.ymlandsrc/test/resources/application.yml - JSON 1.1 only: inject the handler in
AwsJson11Controller - Obtain storage through
StorageFactoryand implementResettable - List any static
RandomorSecureRandomfield under--initialize-at-run-timeinapplication.yml - Check every timestamp member you emit for a
TimestampFormatTraitbefore using the epoch-seconds idiom. It is the awsJson1.1 default, but a model can override it per member, and the mismatch is invisible to the AWS CLI because botocore coerces the value, while strict SDKs (Go, Java) reject the whole response.javap -con the SDK model class shows the traits on eachSdkField - Add
<Svc>ServiceTestand<Svc>IntegrationTest - Document it:
docs/services/<svc>.md, amkdocs.ymlnav entry, a Service Matrix row indocs/services/index.md, and a row in the README category table - Register the handler in
tools/docs/services.yaml, then runmake docs-syncandmake docs-check - Add a
TestFixturesclient factory and a<Svc>Testincompatibility-tests/sdk-test-java
Every type is served by a per-service provisioner under
services/cloudformation/provisioners/. CfnResourceDispatcher only routes a resource to
the registry and stubs what nothing serves; never add type-specific code to it.
- Add the type to the existing
<Service>CfnProvisioner, or create one:@ApplicationScoped, injecting only the service it wraps. CDI discovery viaCloudFormationResourceRegistryhandles registration: no manual wiring, but a missing@ApplicationScopedsilently means the type is never provisioned. resourceTypes()lists theAWS::*types;provision(resource, props, ctx)does the work, switching onresource.getResourceType()when it serves several.- Set both reference mechanisms. They are separate:
resource.setPhysicalId(...)backsRefresource.getAttributes().put(...)backsFn::GetAtt, one entry per attribute Omitting an attribute does not fail;Fn::GetAttresolves to the literal"LogicalId.Attr". Source the attribute names from the type's registry schema inlocal/aws/cfn-resource-schemas/us-east-1/(readOnlyProperties), and validaterequiredfrom the same file.
provisionserves create and update. OnUpdateStackit is re-invoked with the prior physical id and attributes already populated on the resource. Branch withctx.isUpdate()/ctx.priorPhysicalId(), not by reading the id off the resource:provisionassigns the new id as it runs, so a resource-derived check flips mid-method.- Override
delete(...)when the type has a backing delete; tolerate already-deleted viaCfnDeletes.safeDelete, passing the specific "already gone" error codes. Never a catch-all: a real failure such asBucketNotEmptymust propagate so the stack reportsDELETE_FAILED. When the delete needs a create-time attribute rather than just the physical id, overridedelete(StackResource, String). - Register in
src/test/resources/cloudformation/supported-resource-types.tsv(type<TAB>Owner).CfnResourceInventoryTestdiffs that file against the CDI-resolved registry, so it also catches a missing@ApplicationScoped. - Add the provisioner to
CfnProvisionerFixture.inferredProvisioners()when it takes a single service, or a fixture test naming that service silently falls through to the stub arm. - Tests: focused unit test mocking one service (
SqsCfnProvisioner's test is the pattern) plus an integration test asserting the exactFn::GetAttkeys. An unmapped type is stubbed asCREATE_COMPLETEwith a fake ARN, so asserting status alone cannot detect a type that was never wired. Note the engine's constructor is package-private, so tests inprovisioners/mock it. - Run
make docs-syncand commit the result. The resource-type table indocs/services/cloudformation.mdis generated from the step-6 inventory; hand edits faildocs-check. Labels, ordering and notes live intools/docs/cfn_resource_types.yaml. - A schema
readOnlyPropertiesentry you cannot set goes insrc/test/resources/cloudformation/getatt-attribute-gaps.tsvwith a reason;CfnSchemaCoverageTestrequires every unset attribute to be fixed or recorded.
References: SqsCfnProvisioner (smallest), Ec2LaunchTemplateCfnProvisioner
(update-in-place and replacement), LogsCfnProvisioner (reconcile-vs-replace update).
A dependency too heavy for the native image (a native runtime, a large engine, another language
ecosystem) ships as a sidecar: a stateless HTTP service in its own container that Floci starts
lazily over the Docker socket. Sidecars live in
floci-io/floci-sidecars, one directory per sidecar
on the shared sidecar-core, and are published as floci/floci-sidecar-<name>:<semver> on
Docker Hub. They implement that repository's docs/contract.md and carry no AWS vocabulary: the
sidecar answers a generic question, Floci maps its service semantics onto the answer.
Floci-side rules:
- Never add a
sidecars/directory to this repository, never publish afloci/floci:<tag>-<name>suffix, and never default an image to:latest(ImageCacheServicenever re-pulls a cached tag). - The consuming service owns two config knobs,
<name>-image(an exact version) and<name>-url(skip container management), and a<Name>SidecarManagerplus<Name>SidecarClientpair.CedarSidecarManageris the reference: it reads the contract JSON from/healthand fails fast on a contract-major mismatch. - The sidecar releases before the Floci PR that needs it, as a final or an
X.Y.Z-rc.Ntag; the Floci PR pins that tag. A PR that needs an unreleased sidecar cannot pass CI. - Tests: the client's wire contract is covered by a JDK
HttpServerfake; one Docker-gated@QuarkusTestruns the pinned image through the real manager under its ownfloci.docker.resource-namespace. A Floci test imports only Floci classes; the sidecar's own behaviour is tested in its repository. - A sidecar that is a stock upstream image plus a script from the classpath needs no repository but still goes through a manager with the same two knobs.
- Use constructor injection
- Prefer self-explanatory code over comments
- Avoid unnecessary comments
- Always use braces in conditionals
- Never leave a
catchblock empty. If an exception is intentionally tolerated, log it with enough context to diagnose it later. When swallowing really is correct and logging would be noise, name the variableignoredorexpectedand say in a comment why it is safe. A barecatch (Exception e) {}is never acceptable. - Follow existing project patterns
- Use modern Java features only when they improve clarity
- Do not use
var. Write the explicit type. Floci reproduces AWS wire contracts, so the concrete type at a call site is usually the thing under review: whether a value is aLinkedHashMapor aMap, an AWS model type or a JDK one, is exactly what a reviewer needs to see. This covers local declarations, enhanced-for (for (Tag tag : tags)), 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<>(), nevernew 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. Real examples in this repo areapigatewayversusapigatewayv2model types, CDIjakarta.enterprise.inject.Instanceversus the EC2 modelInstance,jakarta.inject.Providerversusjakarta.ws.rs.ext.Provider, and a service's ownRecordmodel versusjava.lang.Record.
- No wildcard imports in
src/main. Static wildcards stay fine in tests, whereAssertions.*,Mockito.*andMatchers.*are the established idiom. - Import order: non-
java/javaximports alphabetically, thenjava.*andjavax.*last. This is the IntelliJ default layout and what most of the tree already uses.
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 parameterized...v()form. No string concatenation in log calls. - No
printStackTrace, anywhere. NoSystem.outorSystem.errinsrc/main; the one exception is the CLI entry pointio.github.hectorvent.floci.tools.ami.AmiImageTool, where stdout is the program's output. A few tests print a failure repro just before failing, which is the only good reason to print from a test: an assertion message usually says it better. java.timefor everything Floci owns.CalendarandSimpleDateFormatappear nowhere and must not be introduced. ADatesurvives only at a third-party boundary that forces one: the BouncyCastle certificate builder, the JAX-RSHttpHeaders.getDate()override, and JDBC'sjava.sql.Datein the RDS Data mapper. Convert at that boundary withDate.from(instant)and keepjava.timeon Floci's side of it.- Constructor injection in
src/main. Field injection is fine in tests, andInstance<T>field injection is a legitimate CDI pattern. Optionalas a return type, and never as a field: there are none, keep it that way. It reaches a parameter only where a Quarkus@ConfigProperty Optional<T>is threaded through; do not introduce it as a parameter for anything else.- Switch expressions over switch statements. Pattern-matching
instanceofover cast-after-check. AwsExceptionfor domain errors.finalon service fields, but not on locals or parameters.
These describe src/test. compatibility-tests is a separate module with the opposite
idiom, AssertJ and @DisplayName in nearly every file. Follow the module you are in.
- Name test methods either as a camelCase sentence (
putAndGetFromMemory) or asmethod_scenario_expectation. Both are established.testXnames are common in older tests and are not the pattern to copy. - JUnit 5 assertions with Hamcrest and RestAssured matchers. AssertJ is a declared test dependency, used by the Lambda launcher tests; prefer the established matchers everywhere else.
@DisplayNameis not used here. The method name carries the intent.
- No em-dashes anywhere, in any content. Use colons, commas, or periods.
- Use JBoss Logging
- Keep logs structured
- Avoid noisy logs in hot paths
- Keep changes focused
- Avoid unrelated refactors
- Preserve behavior unless the task explicitly requires change
- Update docs when necessary
- Explain missing tests when behavior changed but no automated coverage was added
Conventional commits:
feat:fix:perf:docs:chore:
Do not add Co-Authored-By trailers for AI tools in commit messages. Keep attribution limited to human contributors.
- Changes merged into
maindo not automatically imply a stable release - Releases are cut from
mainvia the "Release Cut" workflow (workflow_dispatchon.github/workflows/release-cut.yml), which runs semantic-release: it bumpspom.xml, writesCHANGELOG.md, commits, tags, and creates the GitHub Release release/x.y.xbranches are retired for now- Tags still trigger the publishing workflows (
release.yml)
Treat release workflows as critical infrastructure.
- Identify service and protocol
- Locate an existing implementation to mirror
- Check config impact
- Check storage impact
- Check documentation impact
- Define the minimal useful test plan
- Run relevant tests
- Validate protocol behavior
- Ensure no custom endpoints were introduced
- Verify config and docs updates
- Creating non-AWS endpoints
- Bypassing
StorageFactory - Changing wire formats without tests
- Forgetting YAML updates
- Producing inconsistent URLs or ARNs
- Testing only with raw HTTP
- Introducing unnecessary new patterns
- Adding type-specific code to
CfnResourceDispatcherinstead of a per-service provisioner - Setting a CloudFormation resource's physical id but not its
Fn::GetAttattributes (they are two separate mechanisms, and the miss is silent) - Hand-editing the resource-type table in
docs/services/cloudformation.md, which is generated, runmake docs-syncinstead - Adding a CloudFormation provisioner without a row in
supported-resource-types.tsvor an entry inCfnProvisionerFixture, either of which leaves a type quietly served by the stub arm
If behavior is unclear:
- Prefer AWS behavior
- Then existing Floci behavior
- Then compatibility test expectations
If a task would require broad architectural changes, stop and surface the tradeoffs instead of refactoring across services blindly.