AWS resource plugin (CloudControl-based)
AWS::CodeBuild::ImageBuild gains a versionUri field and matching
res.versionUri resolvable: the full <repo>:<versionTag> reference,
declared by the author and placed as a create-once immutable pin on the
manifest the build pushes. Because the value is declared rather than produced
by the build, a consumer (say, a task definition's container image) that
references res.versionUri is planned in the same apply that rebuilds the
image, so an image roll converges in one apply; deploying through
res.imageRef (the digest, which exists only after the build) takes two.
The digest resolvables are unchanged and remain available.
Resources formae created in order to reach this account are no longer offered
for import. The connect role and the account-global OIDC provider its trust
policy names carry an ownership marker, and discovery now excludes anything
carrying it, so a reconcile can no longer take away formae's own access. EFS
file systems and access points are matched through their per-type tag
properties, which a generic tag filter never sees. Sharing the formae name
prefix is not enough on its own: unrelated roles stay visible.
Discovery for four more resource types: AWS::IAM::ManagedPolicy,
AWS::KMS::Alias, AWS::EC2::PrefixList, and AWS::EC2::Route (scoped per
route table). AWS-managed inventory is kept out of discovery: AWS-managed
policies (arn:aws:iam::aws:policy/*) and reserved aliases (alias/aws/*)
are skipped at list time before the per-resource read, AWS-owned prefix
lists are excluded by owner, and the implicit local route every route table
carries is excluded by gateway.
Every resource type that keeps discoverable = false now carries an inline
comment recording why: no listable inventory, no CloudControl list support,
a defect worth recording, or AWS-managed flooding without a filterable
signal.
Polymorphic auth on the target config: DefaultChainAuth (the existing
default credential provider chain — env vars, shared config, IMDS/IRSA —
optionally pinned to a shared-config profile) or OidcAuth (federated
identity: an OIDC identity token from the paired oidc-credential broker,
exchanged for credentials by assuming a role you name in the target
account). auth and the legacy flat profile field are mutually
exclusive; setting both is rejected at eval.
OidcAuth requires a formae agent with oidc-credential broker support
(minFormaeVersion = "0.89.0" in formae-plugin.pkl). Paired with an
older agent, or an agent with no broker paired, credential resolution
fails closed with an explicit error rather than ever falling back to
ambient credentials.
Known limitation: the STS exchange uses a default-configured client.
The AssumeRoleWithWebIdentity call that turns the identity token into
credentials is made with a region-only STS client, so
AWS_USE_FIPS_ENDPOINT, AWS_ENDPOINT_URL_STS and a custom CA bundle are
not honoured on that one exchange. Proxy settings are honoured, because
they come from the HTTP transport rather than from SDK configuration.
Every other AWS call the plugin makes uses the fully configured client and
is unaffected.
One-time bookkeeping, not drift. Existing targets carry no auth
block, and adding the field to the schema is a change formae records
against every target's stored metadata regardless of whether the target's
declared configuration actually changed. Expect a single, resource-inert
target-metadata update on the first reconcile after upgrading to this
version — no cloud resource is read, created, updated or destroyed by it.
If a stack shows exactly one such update per target immediately after the
upgrade, this is why; it is not drift and does not recur.
AWS::RDS::Database and AWS::RDS::DatabaseRole support. A PostgreSQL
database inside an Aurora cluster, and its owning login role, are now
first-class declared resources. CloudControl models a cluster and its
instances but nothing inside the engine, so both are driven directly over the
RDS Data API — the plugin needs no network path to the database, only
IAM-authenticated HTTPS against the cluster ARN and a Secrets Manager secret
in the standard RDS JSON format. Aurora PostgreSQL only; a cluster running
another engine, or one without the Data API enabled, is rejected with an
explicit error rather than a failed statement.
Modelled on the database: clusterArn, adminSecretArn, databaseName and
owner. On the role: clusterArn, adminSecretArn, roleName, password
and canLogin. Both expose resolvables so a database, its owning role and the
secret holding its credentials are wired through the resource graph rather
than by naming convention. The admin secret is deliberately mutable — an
update proves the replacement credential can reach the cluster before anything
starts depending on it — while the cluster and the object's name are
create-only.
The password never reaches the SQL text. A PASSWORD clause cannot take a
bind parameter, so the plugin composes the PostgreSQL SCRAM-SHA-256 verifier
itself and sends that; the plaintext appears in neither CloudTrail's
rds-data events nor any statement logging. The password field is write-only
and opaque, so it is hashed at rest and never compared against the salted
verifier the engine stores. Rotation is therefore driven from the change being
applied: the password is written when that change touches it, and also when it
carries no usable signal — a missed rotation is a correctness bug, while
writing the same password again converges. Each write stores a freshly salted
verifier, so a redundant one is convergent rather than a no-op. A password the
plugin cannot read is treated the same way. Once formae holds an opaque value
only as a hash it stops sending the value itself, and what arrives in its place
is deliberately not the type the schema declares; an update that is not writing
the password tolerates that rather than failing, so the login attribute and the
admin credential stay changeable, while one that is writing it fails outright
rather than storing something no client could authenticate against.
Passwords must be printable ASCII (U+0020 to
U+007E), the range PostgreSQL's SASLprep normalization passes through
unchanged; Secrets Manager's generated passwords already satisfy this.
Destroying either resource is destructive and has no snapshot or retain
semantics. Destroying a database runs DROP DATABASE, escalating to
WITH (FORCE) — which terminates open sessions — only when the engine reports
the database in use. Destroying a role runs DROP ROLE, which PostgreSQL
refuses while the role owns any object, including objects in databases formae
does not manage; that refusal is surfaced as an actionable error naming the
role. Creating a database also grants the admin membership of the owning role
where PostgreSQL requires it, and that grant is never revoked: it confers
nothing the admin could not re-grant itself, and a revoke could not be made
atomic.
Neither type is discoverable — enumerating objects inside a cluster needs
admin credentials discovery cannot supply — so listing fails with an explicit
unsupported error rather than an empty page. The principal running the plugin
needs rds-data:ExecuteStatement on the cluster,
secretsmanager:GetSecretValue on the admin secret (plus kms:Decrypt on its
key for a customer-managed key), and rds:DescribeDBClusters for the
create-time preflight.
A cluster that is not serving yet is waited out, not failed. An Aurora cluster reports itself available, with the Data API enabled, for several minutes before it can answer a statement — until it has a running DB instance every call is rejected. Creating a database or a role probes the cluster first, and a cluster that cannot answer yet defers the create: the operation reports itself as still running and finishes from a later status check, once the cluster serves. Declaring a cluster, its instance and a database in one forma therefore works, instead of exhausting the retry budget minutes short of the cluster being ready. The wait is bounded — a create still waiting after fifteen minutes fails, naming the cluster — and no statement that changes anything is sent until the cluster answers. A wait interrupted by the plugin restarting fails and asks for the apply to be run again, rather than reporting a create that never ran. Other conditions that clear on their own (an endpoint still coming up, a resuming or unavailable database, a service fault or timeout) are reported to the agent as recoverable on every operation, so it retries them instead of failing the resource. Faults that will not clear — access denied, an unusable secret, a rejected statement — stay terminal, keep their diagnosis, and never start a wait.
Discovery for seven more resource types whose CloudControl list support was
verified against the type registry and a live account: AWS::IAM::User,
AWS::IAM::VirtualMFADevice, AWS::ECS::CapacityProvider,
AWS::RDS::CustomDBEngineVersion, AWS::S3::AccessGrantsInstance,
AWS::Lambda::Permission (scoped per function), and
AWS::ElasticLoadBalancingV2::ListenerRule (scoped per listener). Live
resources of these types now appear in inventory and can be brought under
management.
Discovery for AWS::ApiGateway::Resource, AWS::ApiGateway::Method, and
AWS::CloudFront::Distribution, and extract for
AWS::CloudFront::Distribution. Live API Gateway resources and methods and
CloudFront distributions now appear in inventory and can be brought under
management. Method has no CloudControl list handler, so the plugin
enumerates methods via the API Gateway control plane, scoped per REST API so
methods on the implicit root resource are discovered too.
S3 BucketEncryption: ServerSideEncryptionRule now models
BlockedEncryptionTypes (an EncryptionType listing of NONE/SSE-C), so
buckets that block SSE-C round-trip through extract and reconcile instead of
having the setting stripped on bring-under-management.
AWS::CodeBuild::Project support. A CodeBuild build project is now a
first-class declared resource. CloudControl reports the type as
non-provisionable, so the plugin drives it directly through the CodeBuild API.
Modelled: name, description, serviceRole, source (type, inline
buildSpec, location), artifacts (type, location, name, packaging),
environment (type, computeType, image, privilegedMode,
imagePullCredentialsType, environmentVariables), cache (type,
location, modes), logsConfig (CloudWatch Logs and S3 destinations),
timeoutInMinutes, queuedTimeoutInMinutes, concurrentBuildLimit, tags,
and the assigned arn. A project exposes res.name and res.arn
resolvables, so its service role, its log group and the image build that runs
on it are wired through the resource graph instead of by naming convention.
Note that CodeBuild's update call leaves an unspecified field untouched, so
every modelled field is sent on every update: removing a field from the forma
clears it rather than leaving the previous value in place.
Deliberately not modelled in this version: vpcConfig, secondarySources /
secondaryArtifacts, fileSystemLocations, buildBatchConfig, badge,
triggers (webhooks), and visibility. A project created outside formae is
likely to use at least one of them, and adopting it would silently drop that
configuration on the first update — so the resource is not discoverable
in this version. Discovery can be enabled once the full property surface is
modelled.
AWS::Lambda::Version now exposes a res.functionArn resolvable carrying
its qualified (versioned) function ARN, so a CloudFront distribution's
lambdaFunctionAssociations entry can reference a published version by
reference instead of a hand-edited literal ARN pin. Lambda@Edge rejects
$LATEST, so the association has always needed a versioned ARN; previously
that ARN had to be copied in by hand. Note the caveat this doesn't remove:
every field on Version is create-only, so a function code change publishes
a replacement version rather than updating the existing one. The safe
order is publish the new version, re-point the distribution at it, wait for
CloudFront to propagate the change, and only then delete the old version,
and a replicated Lambda@Edge version can stay undeletable for a while after
the association is removed. Exposing the resolvable removes the hand-edit;
it does not change that ordering.
AWS::ECS::TaskDefinition: ContainerDefinition.image now accepts a
resolvable (String|formae.Resolvable) instead of a bare String, so a
container image can be wired through the resource graph: an
AWS::CodeBuild::ImageBuild digest, an AWS::ECR::Repository URI, or any
other resolvable image reference. It was the only reference-shaped field on
the resource family still typed as a plain string, which forced the image to
be pinned by hand and re-pinned on every rebuild.
AWS::ServiceDiscovery::PrivateDnsNamespace and AWS::ServiceDiscovery::Service
support (AWS Cloud Map). A workload can now be registered for service discovery
from a forma: declare a private DNS namespace, declare a service in it, and
reference that service's res.arn from an ECS service's
serviceRegistries[].registryArn. ECS then registers each task's address as
the task starts and deregisters it as the task stops, so
<service name>.<namespace name> resolves to the live task addresses inside
the VPC. The ECS field already accepted a reference; until now there was no
resource to point it at.
The namespace exposes res.id, res.arn, res.name and res.hostedZoneId
resolvables. The hosted zone id matters because Cloud Map creates and owns a
Route53 private hosted zone for the namespace, and nothing else gives you a
handle on it.
CloudControl reports the namespace type as non-provisionable, so the plugin drives it directly through the Cloud Map API. Its create, update and delete are asynchronous operations that the plugin polls to completion. A delete that AWS rejects because the namespace still holds services is retried under a single timeout rather than failed outright, since ECS deregisters task instances asynchronously after an ECS service is deleted and a stack destroy can legitimately reach the namespace while that is still settling.
Two limitations to know about before adopting this. The namespace's vpc is
create-only and write-only, and the namespace is not extractable: Cloud Map
returns a namespace's VPC from no API and offers no way to change it, so an
extract could never populate the field and a namespace created outside formae
cannot be brought under management by extracting it. Discovery still lists
namespaces; only extraction is unavailable. Second, a namespace name must be
unique within a VPC, and a colliding create is accepted and then fails
asynchronously with a hosted-zone conflict rather than being rejected up front.
On the service, dnsConfig.dnsRecords is replaced as a whole on update, which
is what the AWS API itself does with that list. Health checking is Cloud Map's
own healthCheckCustomConfig for services in a private namespace;
healthCheckConfig is only valid in a public namespace and AWS rejects it
otherwise.
Deliberately not modelled in this version: HttpNamespace,
PublicDnsNamespace, and Instance. Instances are registered and deregistered
by ECS itself, and the other two namespace types are non-provisionable in the
same way as the private one, so each would need its own provisioner rather than
coming for free with a schema.
AWS::CodeBuild::ImageBuild gains additionalTags: immutable pins placed on
the manifest a build pushes, alongside imageTag. imageTag moves to the new
digest on every in-place rebuild, so a consumer that pinned the previous
digest lost the image it pinned — the rebuild left that manifest untagged, and
an untagged predecessor is exactly what the resource prunes. A pin gives the
predecessor a name of its own, so it is not untagged, so it survives: a
rollback target that outlives the next build.
Pins are create-once. A pin declared for the first time is placed on the manifest that build produced, but only if the tag does not already exist — a newly declared pin whose tag is taken fails the apply, naming the tag and the digest it currently resolves to, so a reused release name cannot yield a green apply whose rollback pin names an unrelated image. A pin carried over from the previous apply is left exactly where it is. The plugin never moves and never deletes a pin. Placement re-registers the identical manifest bytes and media type, so the pin resolves to the same digest by construction, and adding one does not force a rebuild — the build that produced the image is not re-run to give it a second name.
Two consequences to declare against. Removing a pin from the listing is a
no-op in the registry, so a placed pin is removable only out of band or by a
lifecycle policy. And a repository that accumulates pins cannot be emptied by
tearing the ImageBuild down: teardown removes only imageTag, while every
pin is by design a manifest meant to outlive the build that produced it. Its
lifecycle becomes yours to manage — a repository lifecycle policy that ages
pins out, or an out-of-band delete. A formae-managed AWS::ECR::Repository
that still holds images cannot be destroyed, and emptyOnDelete does not
currently change that: CloudControl's delete carries no resource model, so the
repository's delete handler never sees the property.
Secrets Manager secrets adopt formae's first-class secret types. A secret's
value can now be referenced with secret.res.secretValue, and
secret.res.secretValue.json("path") reaches into a JSON payload. The
existing res.secretString, res.arn, res.id and res.name accessors
keep working unchanged.
Every secret-bearing property can now be bound to a formae generator, so the
credential is drawn (and rotated) by the agent instead of being pinned in the
forma file. Twelve fields accept a generator output, written
pw.gen.value: AWS::EC2::VerifiedAccessTrustProvider
OidcOptions.clientSecret, AWS::EC2::VPNConnection
VpnTunnelOptionsSpecifications.preSharedKey,
AWS::ElasticLoadBalancingV2::Listener and ...::ListenerRule
AuthenticateOidcConfig.clientSecret, AWS::IAM::ServerCertificate
privateKey, AWS::IAM::User LoginProfile.password,
AWS::Lambda::Permission eventSourceToken, AWS::RDS::DatabaseRole
password, AWS::RDS::DBCluster masterUserPassword,
AWS::RDS::DBInstance masterUserPassword and tdeCredentialPassword, and
AWS::SecretsManager::Secret secretString. Every one of these fields was
already opaque and stays opaque, so a generated value is still hashed at
rest. Literal strings, formae.Value, formae.SecretValue and (where the
field already accepted one) formae.Resolvable keep working unchanged, and
the two fields that carry a pattern constraint on the literal
(ServerCertificate.privateKey, Permission.eventSourceToken) still reject
a string that does not match.
AWS::RDS::DBInstance no longer models certificateDetails as a schema
property. The field is read-only on AWS's side and its contents move on
their own: RDS rotates the server certificate automatically around its
half-life, so treating the details as a declarable property with a provider
default made an unchanged forma reject reconciles after every rotation. The
values are still stored with the resource and still referenceable through
the certificateDetailsCAIdentifier and certificateDetailsValidTill
resolvables. A forma that declared certificateDetails now fails at eval;
delete the block, the field never accepted a user value.
Every hasProviderDefault schema annotation now carries a recorded
disposition in schema/provider-default-dispositions.json, enforced by a
unit test: new annotations fail CI until classified, and rows for removed
fields fail as stale. Fields confirmed as provider-default-once are marked
keep with a pin; fields a co-actor legitimately writes (such as
AWS::RDS::DBInstance.engineVersion, which RDS moves under auto minor
version upgrades) are marked co-owned while their handling is designed.
AWS::S3::Object is no longer discoverable. A bucket's object count is
unbounded, and objects are data rather than infrastructure, so a discovery
scan enumerated every key in every bucket in the account — slow enough to
stall the scan on a real account, and it filled the inventory with rows
nobody had declared. Objects formae manages are created, read, updated and
destroyed exactly as before; they are simply no longer found by discovery, so
an existing object now has to be declared to be brought under management.
AWS::CodeBuild::ImageBuild prunes the predecessor manifest after an in-place
rebuild — behaviour present since the resource landed and not previously
written down. A rebuild moves imageTag to the new digest and leaves the
prior manifest untagged; the prune deletes it, so a co-managed repository
stays empty enough to tear down. It only ever deletes a manifest that carries
no tag at all, which is what lets the new additionalTags retain a
predecessor without changing the prune.
Breaking. AWS::CodeBuild::ImageBuild is now a pure build-and-push
action: it creates no IAM role, no CodeBuild project and no log group.
It runs one build on a project you declare and name, and its only effect in
the account is the pushed image. Previously (0.1.15) it idempotently created
and updated an internal IAM service role and CodeBuild project, leaving
resources in the account that no forma described and that no audit of the
forma would predict.
To migrate a forma:
projectName, resolved from the declared project's res.name;serviceRoleArn, computeType, buildEnvironmentImage and
timeoutMinutes; their equivalents are now Project properties
(serviceRole, environment.computeType, environment.image,
timeoutInMinutes);AWS::CodeBuild::Project, the AWS::IAM::Role it runs as, and
the AWS::Logs::LogGroup it logs to. The project must use a privileged
LINUX_CONTAINER environment with source.type = "NO_SOURCE" and
artifacts.type = "NO_ARTIFACTS"; a project that does not is rejected with
a message naming the offending value before any build starts. The project's
own build spec is a placeholder — the image build supplies the spec it runs
per build as an override, and never reads the project's.The flat profile field on the target config is deprecated in favor of
auth = new DefaultChainAuth { profile = ... }. It continues to work
unchanged — a target set this way still authenticates via the default
credential chain pinned to that profile — and logs one deprecation warning
per plugin process rather than on every call. There is no removal in this
release; flat profile will be removed at a future major version, posted
ahead of time.
Migrating is a normal target update. Both profile and auth are
declared mutable, so rewriting a target from the flat profile to an
auth block changes the target's configuration in place and touches no
cloud resource.
This holds only on an agent at or above the release that carries these
hints. An older agent does not see them, classifies the dropped
top-level Profile key as an immutable change, and plans a target
replace, which destroys and recreates every resource on that target.
Upgrade the agent first, then migrate.
Updating a writeOnly property no longer fails. Cloud Control accepts only an
add operation for a property it marks writeOnlyProperties, since such a
property is absent from every read and there is nothing there to replace, and
it rejected the replace that formae plans. Cloud Control names the offending
paths when it rejects one, so the plugin now rewrites exactly those operations
and resends. This covers every resource type without the plugin having to know
which properties are writeOnly ahead of time: AWS::Lambda::Function has ten
such properties (Code/ZipFile among them, so an inline-code function could
not be changed at all) and AWS::RDS::DBInstance sixteen. It replaces the
hardcoded special case that did the same for AWS::SecretsManager::Secret
SecretString alone.
AWS::RDS::Database and AWS::RDS::DatabaseRole no longer fail when the
cluster's admin credential is momentarily refused. RDS rotates an RDS-managed
master-user secret on its own schedule, starting within a minute of the
cluster being created, and while a rotation settles the Data API rejects some
statements authenticated with that secret while sibling statements carrying
the same secret succeed a second either side. That authentication failure was
classified as an invalid request, which the agent does not retry, so a
create landed on a coin flip. It is now reported as not-yet-stabilized: the
readiness probe parks the create and polls, and a rotation that lands
mid-statement is retried.
AWS::CloudFront::Distribution Origin.originCustomHeaders[].headerValue is
now opaque. A custom origin header is the documented way to stop callers
reaching an origin directly: CloudFront sends the header and the origin rejects
requests without it, which makes the value a shared credential, but it was
typed as a plain String and so was stored in cleartext. headerName is
deliberately left non-opaque, because it is the identity half of the pair and
is used to match a header across a diff.
AWS::ApiGateway::ApiKey value is now opaque, so an author-supplied API key
is hashed at rest instead of being stored in cleartext. The field carries the
credential clients present in the x-api-key header, but it was typed as a
plain String, and a field is marked opaque from its type: without a
formae.SecretValue in the union the agent had nothing telling it to hash the
value, so a declared key persisted in the clear. It now names
formae.ValueSource like every other credential field here, so it also
accepts a generator binding. It still accepts a literal string, and AWS still
generates the key when none is declared, so a forma that does not set value
is unaffected.
AWS::Lambda::Version discovery now surfaces published versions. The list
post-filter compared the parent function's name against the ARN form
CloudControl echoes back, dropping every listed version, so version
discovery silently found nothing.
AWS::Lambda::Permission discovery now finds permissions attached to a
published version or an alias, not only those on the bare function. Lambda
keeps a separate resource policy per qualifier and the CloudControl list
only returns the unqualified one, so a permission granted against a
version's or alias's ARN (the shape API Gateway and Lambda@Edge wiring
uses) was invisible to discovery. The plugin now walks every policy scope
through the Lambda control plane.
An AWS::CertificateManager::Certificate with subjectAlternativeNames no
longer plans a destructive replace on every reconcile. ACM injects the
certificate's primary domainName into the subject alternative names it
returns from DescribeCertificate, whatever was requested, and
subjectAlternativeNames is createOnly — so a certificate declaring only
the additional names differed from the read-back on an immutable property and
every reconcile planned a destroy-and-recreate of an issued certificate,
cascading to the DNS validation records wired to its resolvables. The
read-back now reports the names other than the primary domain, so an
unchanged certificate reconciles as a no-op, while a genuine change to the
list still replaces the certificate. Declared names are unchanged on the way
out: whatever the forma lists is still sent to ACM verbatim.
The canonical way to declare subjectAlternativeNames is therefore the
additional names only, excluding the primary domainName. If your forma
lists the primary domain in subjectAlternativeNames — the workaround for
this bug — it will plan a replacement of the issued certificate once you pick
up this release. Remove the primary domain from the list. That edit is
declaration-only: ACM stores the same certificate either way, so the
declaration converges against the existing certificate with no cloud change.
Simulate before applying to confirm the plan is empty.
Resources are no longer reported as failed while AWS is still rate-limiting a request that would have succeeded. When CloudControl throttled a call, the plugin absorbed the retries inside a single request to the agent, which could take around three minutes and left the agent with no sign of progress; the agent's check for an unresponsive plugin fired after 40 seconds and failed the resource, cascading to everything that depended on it. Applying many resources at once made this most likely, and subnet creation was the usual casualty. The plugin now bounds how long any one request may spend retrying and reports the work as still in progress instead, so the agent sees continuous progress while the retries continue. Requires formae 0.89.0 or later for the matching change on the agent side.
A Secrets Manager resource policy is now replaced as a whole document when it changes. Individual statements were compared as a set, so a patch-mode update that edited a statement added the new one without removing the old, leaving the live policy holding both. This was previously masked by a separate defect that recreated the policy on every update; with that fixed in formae 0.89.0, policy updates apply in place and need this hint to be correct.
AWS::CloudTrail::Trail support. You can now author and discover CloudTrail trails declaratively. Management and S3 data-event selectors are modelled, including advanced event selectors with resources.ARN StartsWith; selector lists apply as atomic (wholesale-replace) updates to match put-event-selectors, and booleans AWS echoes on read are treated as provider defaults to avoid perpetual drift. Discovery labels a trail by its TrailName (trails carry no Name tag), and AWS::S3::BucketPolicy gains a resolvable so a trail can order after its delivery bucket's policy.AWS::CodeBuild::ImageBuild support. You can now build a container image from a supplied Dockerfile via AWS CodeBuild, push it to ECR, and pin a downstream consumer (an ECS task definition, a Kubernetes pod) to exactly the image produced, making an image build a declarative step inside formae instead of an out-of-band docker build/push. The resource idempotently ensures an internal IAM service role and CodeBuild project (or adopts a supplied serviceRoleArn), runs the build, and exposes the pushed image's immutable digest as resolvables, imageRef assembled as repo@sha256:… from the exported digest, alongside imageDigest and imageUri, so a consumer pins to the digest rather than a mutable tag. AWS::ECR::Repository gains a repositoryUri resolvable to feed the build's target repository.formae.SecretValue so their values are hashed at rest end-to-end (previously stored in cleartext on the read/actual-state path). Covers AWS::SecretsManager::Secret secretString; AWS::RDS::DBInstance masterUserPassword/tdeCredentialPassword; AWS::RDS::DBCluster masterUserPassword; AWS::EC2::VerifiedAccessTrustProvider, AWS::ElasticLoadBalancingV2::Listener and AWS::ElasticLoadBalancingV2::ListenerRule clientSecret; AWS::EC2::VPNConnection preSharedKey; AWS::IAM::ServerCertificate privateKey; AWS::IAM::User login-profile password; and AWS::Lambda::Permission eventSourceToken. Requires a formae agent on the matching release.minFormaeVersion is bumped to 0.88.0 accordingly.AWS::EKS::Cluster no longer plans a destructive replace on every reconcile. The parent accessConfig field was annotated createOnly, writeOnly, and hasProviderDefault at once: writeOnly stripped accessConfig from the read-back actual state, so an unchanged declared accessConfig appeared only in the desired state and emitted a spurious add op, and because the field was also createOnly that phantom op flagged the cluster for a roughly fifteen-minute destroy-and-recreate that takes every workload on it down. The annotations now match the CloudControl contract: accessConfig keeps only hasProviderDefault (AWS auto-populates authenticationMode, a mutable in-place field), and the write-once bootstrapClusterCreatorAdminPermissions child is createOnly alongside its existing writeOnly. An unchanged cluster now reconciles as a no-op and an auth-mode change applies in place instead of replacing the cluster.ListObjectsV2 must be addressed to a bucket's home region; a bucket in a different region than the configured client answered with a 301 PermanentRedirect, so discovery logged a list error and skipped that bucket's objects. The list path now reads the home region from the redirect's x-amz-bucket-region header and retries the request against that region.LoggingConfiguration discovery no longer errors every cycle. CloudControl does not support the LIST action for AWS::NetworkFirewall::LoggingConfiguration (it returns UnsupportedActionException), so background discovery hit a 400 on this type each cycle. A logging configuration is a per-firewall singleton, so the plugin now registers a custom List, scoped to one firewall via the FirewallArn list parameter and reading that firewall's logging configuration, with the Firewall declared as the discovery parent.ReadRequest.PriorProperties, which the agent populates from formae 0.87.1. minFormaeVersion is bumped to 0.87.1 accordingly.Role whose inline policies are managed as standalone AWS::IAM::RolePolicy resources no longer has its updates rejected or its sibling-managed policies wiped. The role's read enriched Properties.Policies with the role's inline policies whenever the role had any, regardless of how the caller modelled them; for a caller managing them as standalone resources the stored row has no Policies key, so the enriched read registered as drift, rejecting every pending update on the role and risking a reconcile that wiped the sibling-managed policies (since Role.policies is atomic). Enrichment is now gated on the caller's prior model: policies are embedded only when the prior model is unknown (a create, a status read-back, or discovery) or already declares Policies; a known prior that omits Policies suppresses the embed. This keeps the no-phantom-drift behaviour for roles that declare inline policies (0.1.13) while unbreaking callers that manage them as standalone resources.AWS::Events::EventBus, AWS::Events::Archive, and AWS::Events::Rule. You can now manage custom event buses, their archives, and their rules, including the full rule target tree, declaratively. A rule wires to its bus and its targets through the resource graph, and each bus exposes its ARN as a resolvable (eventBus.res.arn), so a producer's events:PutEvents permission can reference the bus directly. Rules are modelled as children of their event bus so that rules on a custom bus are discovered correctly.Table now exposes resolvables, table.res.arn, table.res.streamArn, and table.res.tableName. A table's stream ARN can be wired straight into a Lambda::EventSourceMapping's eventSourceArn, so a stream-triggered Lambda no longer needs a hand-constructed stream ARN, previously there was no way to reference it at all.RestApi now exposes an execute-api ARN resolvable, api.res.executeApiArn. A Lambda::Permission that lets API Gateway invoke a function can source its sourceArn from the API itself (api.res.executeApiArn) instead of a hand-built, account-scoped wildcard ARN. The plugin derives the ARN, which CloudControl doesn't return, and fills in the partition and account.AWS::IAM::ServerCertificate now exposes resolvables, serverCertificate.res.arn and serverCertificate.res.serverCertificateName. Other resources (for example an HTTPS load balancer listener) can now reference an uploaded server certificate through the resource graph instead of a hand-written ARN.AWS::S3::Object support. You can now manage S3 objects declaratively, including shipping a local file as the object's body: the CLI reads the file relative to the apply working directory and uploads it. Object tags round-trip on create and update.AWS::S3::Object can now fetch its body from a URL. Instead of inline content, the object's source can be a structured remote source, the agent downloads the body over HTTPS at apply time, so the bytes never pass through the CLI. The fetch can send request headers (to pull an authenticated artifact; the header value is write-only and is not stored in cleartext) and can extract a named file from a downloaded zip archive. A typical use is delivering a Lambda deployment package from a versioned build artifact: point the object at a release URL templated by version and resolve the function's Code from the object's version. Publishing a new build then redeploys the function, while re-applying the same version is a no-op, no phantom redeploys. The fetch is restricted to HTTPS and refuses to reach loopback, private-network, or instance-metadata addresses. A full walkthrough, handler, release, deploy, and the redeploy loop, is in the lambda-http-source example.Function's functionCode can now embed references to other resources. Using formae 0.87.0's formae.embed, a function's JavaScript body can splice in another resource's generated value (for example a Key Value Store's Id) at apply time, instead of applying the store, copying the Id by hand, and re-applying the function.AWS::EC2::VPNGatewayRoutePropagation support. You can now manage VPN gateway route propagation declaratively, having a virtual private gateway automatically propagate its learned routes into a route table, and removing that propagation again. This type can't be provisioned through CloudControl, so it previously couldn't be managed at all; the plugin now drives it directly through the EC2 API.AWS::EC2::NetworkInterfacePermission support. You can now grant (and revoke) another AWS account permission to attach or associate one of your network interfaces. This type can't be provisioned through CloudControl, so it previously couldn't be managed at all; the plugin now drives it directly through the EC2 API. A network interface can also now be referenced by other resources through the resource graph (someInterface.res.id).AWS::Route53::RecordSetGroup support. You can now manage a group of Route 53 records that are created, updated, and deleted together in a single atomic change, useful when a set of records must always change as a unit. This type can't be provisioned through CloudControl, so it previously couldn't be managed at all; the plugin now applies the whole group through one Route 53 change batch. Scope is simple records (name, type, TTL, values, and alias targets); weighted, latency, geolocation, and other routing-policy records are rejected with a clear error rather than silently dropped.AWS::IAM::UserToGroupAddition support. You can now manage an IAM group membership, adding a user to a group and removing it again, declaratively. This type can't be provisioned through CloudControl, so it previously couldn't be managed at all; the plugin now drives it directly through the IAM API. Each resource models one user-in-group membership, so to add several users to a group you declare one UserToGroupAddition per user.Function.functionCode, above), and the new field hint for values a provider drops unless they are re-sent on every update. Secret- and configuration-class fields that need that treatment (an OIDC client secret, several ECS service-configuration fields, EC2 Launch Template write-only fields, and an IAM user's initial console password) are now annotated so they keep applying on update under 0.87.0's revised write-only behaviour. minFormaeVersion is bumped to 0.87.0 accordingly.pkl eval time, so an invalid value is caught when the forma is evaluated rather than rejected deep in the apply by AWS.Role with inline policies no longer shows a phantom update on every reconcile. AWS stores a role's inline policies separately and CloudControl's read doesn't return them, so formae re-proposed adding them on every reconcile even though they were already present. A role's inline policies are now read back, so a role that hasn't changed reconciles as a no-op. Note: manage a role's inline policies through policies or as standalone AWS::IAM::RolePolicy resources, not both on the same role.Lambda::EventSourceMapping no longer plans a destructive replace on every reconcile. Its FunctionName was incorrectly treated as immutable; because AWS reads the field back as the function's full ARN while a forma typically declares the short name, every reconcile saw a "change" to an immutable field and planned a destroy-and-recreate of the mapping. FunctionName is now correctly mutable (AWS updates it in place), so the mapping reconciles without a replace. Reference the target function by its ARN (someFunction.res.arn) for a clean no-op.Method with a Lambda-proxy integration no longer shows a phantom Integration update on every reconcile. The function reference is written into the integration as an invocation URI, but the read didn't translate it back, so the stored integration never matched what the forma declared. The read now restores the function reference from the invocation URI, and the same translation is applied when the integration is updated, so re-pointing a method at a different Lambda applies as an in-place update instead of failing.AWS::NetworkFirewall::Firewall, AWS::NetworkFirewall::FirewallPolicy, AWS::NetworkFirewall::RuleGroup, and AWS::NetworkFirewall::LoggingConfiguration. You can now manage Network Firewall egress controls declaratively, including FQDN-allowlisting outbound traffic from private subnets. Rule groups, policies, and the firewall wire together through the resource graph (policy and rule-group ARNs, the firewall ARN, VPC and subnet IDs, and the log group are all Resolvables). The firewall exposes its per-AZ endpoints as a resolvable map, firewall.res.endpointIds.at("<az>"), so a route table can send 0.0.0.0/0 through the firewall endpoint in its own availability zone; existing EC2 routes accept a VPC-endpoint target unchanged. The firewall withholds create/update success until those per-AZ endpoints have propagated, so routes that depend on them don't resolve against an endpoint that isn't ready yet.FirewallPolicy's default-action lists relies on the whole-list replace behaviour added in formae 0.86.2; on earlier agents the update sends the old and new actions together and AWS rejects them as mutually exclusive. minFormaeVersion is bumped to 0.86.2 accordingly.INFO/WARN lines, such as recoverable CloudControl throttling retries, surfaced as ERROR and carried none of those attributes, making the agent log noisier and harder to filter by resource.CNAME, DNAME, NS, PTR, MX, or SRV record (or an ALIAS target) declared without the trailing dot read back dotted and produced a perpetual no-op diff. Because record-set updates are applied as delete-then-create, that mismatch could also fail with InvalidChangeBatch and block the apply. formae now normalises the trailing dot on read for these record types, so a dot-less declaration reconciles as a no-op. TXT and SPF values (quoted character strings) and A/AAAA (IP addresses) are left untouched.CNAME's value is sourced from the certificate via cert.res.validationRecords, and AWS returns it with a trailing dot while the record set read it back without one, leaving a phantom update on every reconcile. The certificate-sourced value is now normalised the same way, so the validation record settles as a no-op.secret.res.arn on AWS::SecretsManager::Secret now resolves to the secret's ARN. It previously failed to resolve at all, so any resource referencing a secret's ARN that way errored out before any AWS call was made. secret.res.arn, secret.res.id, and secret.res.ref all now resolve to the ARN.secretString on AWS::SecretsManager::Secret previously had no effect, the value never reached AWS, so the secret didn't rotate. It's now applied, and opaque.setOnce is honoured so an unrelated edit won't re-write a set-once value. Requires formae 0.86.2, this release's floor.AWS::SES::EmailIdentity updates now apply reliably. CloudControl's asynchronous update handler for this type can fail with GeneralServiceException ("The security token included in the request is invalid"), failing an otherwise-valid update. The plugin now applies EmailIdentity updates directly through the SES v2 API, MAIL FROM, DKIM signing, feedback forwarding, configuration set, and tags, instead of routing through CloudControl, mirroring how its Read is already handled.AWS::EKS::Cluster no longer shows a phantom update on every reconcile. Newer Kubernetes versions (1.32 and later) return a control-plane egress mode that AWS populates itself, which formae previously treated as unexpected drift. It is now recognised as a provider-managed default, so a cluster that hasn't changed reconciles as a no-op.AWS::CertificateManager::Certificate, AWS::CloudFront::Function, AWS::CloudFront::KeyValueStore, AWS::CloudFront::CachePolicy, AWS::CloudFront::OriginRequestPolicy, AWS::CloudFront::ResponseHeadersPolicy, and AWS::CloudFront::OriginAccessControl. ACM certs ship with a full custom provisioner that talks to the ACM API directly (the resource type is non-provisionable through CloudControl); cert.res.validationRecords exposes the DNS validation CNAMEs ACM publishes, so a Route53::RecordSet (or any other DNS-publisher resource) can wire them through the resource graph instead of being filled in by hand. CloudFront Distributions can now reference all of the above through Resolvable links, cache policy ID, origin-request policy ID, response-headers policy ID, origin access control ID, function ARN, lambda ARN, and ACM cert ARN, and formae orders creates and destroys correctly based on those references.AWS::ECS::Service now exposes an endpoints resolvable (service.res.endpoints.at("containerName:containerPort")) so downstream resources can wire their config URL through the service itself instead of through the listener. Because the endpoint resolves only once the service is operationally stable (the deployment-stability gating added in 0.1.10), anything consuming it waits until tasks are actually serving traffic. This closes the fresh-apply race where a listener URL resolved seconds before the tasks behind it were healthy, leaving consumers pointed at an endpoint returning 503s. Alongside this, AWS::ElasticLoadBalancingV2::ListenerRule now exposes its target group ARN so consumers of rule-routed services get correct ordering instead of racing the rule, and a load balancer name longer than the AWS 32-character limit is now rejected at pkl eval time rather than deep in apply. Listener-rule path/host routing, weighted target groups, and NLB endpoints are deferred follow-ups.createOnly, to line up with the new planning behaviour in formae 0.86.0. Under 0.86.0, fields that aren't explicitly immutable are treated as mutable and updated in place rather than triggering a replace. Any field AWS actually rejects on update needs to be marked immutable, or the apply fails at the provider. With this release every such field is annotated correctly, so the 0.86.0 in-place-update behaviour lands without surprise provider rejections. minFormaeVersion is bumped to 0.86.0 accordingly.AWS::EC2::SecurityGroupIngress and AWS::EC2::SecurityGroupEgress had no incoming edges in the destroy graph, so they were torn down first, severing ALB-to-task health checks, task-to-EFS NFS, and task-to-internet connectivity, which then cascade-failed the rest of the teardown. The destroy order is now workloads, then the security group rules, then the security group itself.AWS::Lambda::EventInvokeConfig no longer fails intermittently on create. CloudControl injects an empty DestinationConfig (with empty OnFailure/OnSuccess sub-objects) into every read of this resource, even when you never set one. formae's required-field validation then walked into the injected empty object and reported a missing Destination, surfacing as a flaky apply failure. The empty sub-objects are now stripped on read; genuine user-set destinations are non-empty and pass through untouched.runtimeDependency) that pull the mount targets into the destroy ordering ahead of their file system.AWS::ECS::Service now reports success only once the deployment is operationally stable: the rollout has completed, the running task count matches the desired count, and at least one healthy target exists behind each attached target group. Previously the service reported success as soon as CloudControl acknowledged the request, so any downstream resource reachable through the load balancer (for example a Grafana target driving its config through the listener URL) frequently hit 503s before the tasks were serving traffic. Non-standard service shapes (CODE_DEPLOY and EXTERNAL deployment controllers, the DAEMON scheduling strategy, classic-ELB attachments without a target group ARN, and desiredCount = 0) fall through to safe defaults rather than waiting on target health.formae apply. The target group's Targets field is populated at runtime by ECS Services (and anything else calling the register-targets API), and LoadBalancerArns is populated when a listener attaches the target group to a load balancer; neither is meaningfully user-settable. Tracking them in formae state meant the periodic synchronizer rewrote the resource on every ECS task placement and every listener attach, after which reconcile rejected the next apply with the stacks-have-been-modified error even though the forma hadn't changed, forcing operators into force mode or extract-and-absorb on every reconcile. Both fields are now dropped from the schema and stripped from the AWS read response before they reach formae state.Service creation no longer fails with target group <arn> does not have an associated load balancer when the Service and its Listener are scheduled together in the same apply. The plugin now treats that specific CloudControl error (InvalidRequest + matching message text, on Create operations only) as a transient in-progress state; the PluginOperator's existing status-poll loop absorbs the race until the Listener finishes wiring the target group to the load balancer. No PKL change needed, direct tg.res.targetGroupArn references keep working and don't have to be rewritten through listener.res.targetGroupArn to avoid the race.attachesTo field hints on Service.LoadBalancer.targetGroupArn and Service.VpcLatticeConfiguration.targetGroupArn. AWS rejects target-group deletion while a Service is still attached; without the annotation, the Service tore down in parallel with the listener chain, and any plugin-target driving CRUD through the Service's listener URL (Grafana, Loki, Tempo, and similar) wedged mid-tear-down with no URL backing it. With the annotation, the Service is destroyed before its target group. Requires formae 0.85.0 or newer to take effect: 0.84.0 agents silently ignore the annotation (the plugin still installs and every other fix applies, but the destroy-edge inversion no-ops). The plugin's minFormaeVersion stays at 0.84.0 deliberately, sibling plugins built against the same SDK family work fine on 0.84.0 and forcing an agent upgrade for one annotation isn't worth the disruption.CloudControl errors are no longer silently terminal. Synchronous errors from CCAPI on Create, Update, and Delete were previously surfaced to the agent as bare Go errors, which the agent classified as UnforeseenError, a non-recoverable code that bypasses the retry pipeline entirely. Even errors that AWS itself flags as recoverable (Throttling, NotStabilized, ResourceConflict, …) ended up as terminal failures. The plugin now translates these into the typed OperationErrorCode formae's PluginOperator understands, so the agent retries recoverable conditions instead of failing the whole apply.AWS::RDS::DBSubnetGroup no longer fails non-deterministically when its subnets are created in the same forma. AWS RDS rejected freshly-created EC2 subnets with InvalidRequestException: Some input subnets ... are invalid until RDS's internal subnet cache caught up, a classic cross-service eventual-consistency window between EC2 and RDS. The plugin recognises this specific class of InvalidRequest as a recoverable race, so the agent retries through the propagation gap and the subnet group lands on the first apply.AWS::SES::ConfigurationSetEventDestination updates now succeed. Previously, any update to an event destination (toggling enabled, changing matchingEventTypes, switching destination targets) failed within milliseconds with no AWS API round-trip. CCAPI rejects this resource's composite <csName>|<edName> identifier on Update with ValidationException: not valid for identifier [/properties/Id], the same limitation that affected Read in 0.1.7. Updates now route through sesv2.UpdateConfigurationSetEventDestination directly.AWS::SES::ConfigurationSetEventDestination deletes now succeed. Same CCAPI composite-identifier limitation as Update. The most user-visible symptom was that formae destroy failed on any stack containing an event destination, and replace flows (when the parent ConfigurationSet.name changes) failed at the destroy step. Deletes now go through sesv2.DeleteConfigurationSetEventDestination, with a missing-destination error treated as a successful no-op so retried destroys are idempotent.AWS::SES::ConfigurationSetEventDestination now correctly surfaces existing event destinations as unmanaged resources. CCAPI's ListResources returns bare EventDestinationNames ("bounces") instead of the composite <csName>|<edName> the resource's Read path requires, so every discovered destination failed its per-resource Read and never made it into the inventory. The plugin's List now walks ListConfigurationSets → GetConfigurationSetEventDestinations and emits properly-formed composite identifiers.AWS::SES::EmailIdentity covers verified sending domains or addresses (with bundled MAIL FROM, feedback, and Easy DKIM attributes). AWS::SES::ConfigurationSet and AWS::SES::ConfigurationSetEventDestination route bounce/complaint/delivery events to SNS, Kinesis Firehose, EventBridge, or CloudWatch, exactly one of the four destination types is enforced at PKL evaluation time, so bad shapes fail at pkl eval rather than at apply time. AWS::SES::EmailIdentityVerification is a polling gate downstream consumers depend on for send-readiness; it sits between the identity (and the DNS records that verify it) and any resource that needs to send mail, breaking the apply-time deadlock where verification needs DNS, DNS depends on the identity, and the identity can't wait on either.EmailIdentity.res.requiredDnsRecords is a typed listing resolvable that exposes the DNS records SES expects, 3 DKIM CNAMEs, plus an MX and SPF TXT for MAIL FROM when configured. A forma drives Route53 (or any DNS plugin) directly off id.res.requiredDnsRecords.at(N).name and .values, with no manual token extraction. See examples/ses-basic/main.pkl in the plugin repo for the full pattern. Terraform, Pulumi, and Crossplane all force users to extract verification_token/dkim_tokens[] strings and hand-author the records; this is the first IaC tool to wire them automatically.AWS::EC2::PlacementGroup's spreadLevel is now marked hasProviderDefault. AWS auto-populates SpreadLevel after a partition-strategy create even when not specified, which previously caused replace flows (where the user switches strategy from spread to partition) to fail with Property SpreadLevel is not expected and not a provider default. The field stays createOnly.AWS::ECS::TaskDefinition are no longer silently dropped from the diff. 19 user-canonical sub-fields on ContainerDefinition, including environment, portMappings, mountPoints, secrets, command, entryPoint, dependsOn, extraHosts, and dockerLabels, were previously annotated hasProviderDefault, which symmetrically stripped them from both the desired and actual sides before comparison. Any real change to those fields produced an empty plan on formae apply, forcing operators into out-of-band aws ecs register-task-definition workarounds (which then showed up as drift on the next reconcile). The annotation is now scoped to the genuinely cloud-defaulted scalars (cpu, essential, versionConsistency).AWS::Lambda::LayerVersion's compatibleArchitectures, compatibleRuntimes, and description are now marked hasProviderDefault. AWS Read returns empty values for these when the user omits them in PKL, and because every LayerVersion field is createOnly, the resulting phantom drift would schedule a destroy + create on every reapply.AWS::Lambda::EventInvokeConfig no longer fails with Model validation failed: required key [Destination] not found. CloudControl's update handler for this resource type re-validates the full server-side state on every patch, even fields the patch doesn't touch, and AWS materialises empty DestinationConfig.OnFailure / OnSuccess sub-objects into the response on Read whether or not the caller ever set them, which then fail the schema's "if present, Destination is required" rule. Updates that only changed MaximumRetryAttempts or MaximumEventAgeInSeconds would still get rejected on the server side. Updates now route through the Lambda UpdateFunctionEventInvokeConfig API directly, which has no such cross-field re-validation.Listener.defaultActions[*].forwardConfig and ECS Cluster.clusterSettings are now marked hasProviderDefault. ELBv2 derives ForwardConfig (target groups + stickiness defaults) on Read from the action's TargetGroupArn when the user specifies a simple forward target, and ECS populates clusterSettings with default entries like containerInsights: disabled when none are configured. Without the annotations, every reapply emitted a no-op patch on these fields that surfaced as a spurious update.Addon, AccessEntry, FargateProfile, PodIdentityAssociation, and IdentityProviderConfig are now first-class resources alongside Cluster and Nodegroup, enabling end-to-end EKS cluster management (including discovery of cluster-child resources) from a single forma.ExpressGatewayService support. Express Gateway services can now be managed through formae. Uses the native ECS SDK because the CloudControl handler for this type is broken server-side.url property that combines the parent ALB's DNS name with the Listener's protocol and port. Use listener.res.url to wire load balancer endpoints into target configs without manual URL construction.formae.Resolvable, and all resources with Resolvable classes now have hidden res wired up.profile field is now mutable, changing it updates the target in place without recreating resources. The region field remains immutable; changing it triggers a full target replace as before. See Per-field config mutability for details.Service and TaskSet discovery no longer spam InvalidRequestException: Missing Or Invalid ResourceModel property errors on every cycle. Service is now discovered as a child of Cluster (its list handler requires a Cluster filter), and TaskSet is enumerated via the ECS SDK's DescribeServices because its CloudControl list handler demands an Id and is effectively a Read.taskDef.res.taskDefinitionArn now works as expected, enabling ECS Services to reference task definitions via resolvables.EFSVolumeConfiguration.filesystemId now accepts resolvable references, so ECS tasks can reference EFS filesystems created in the same forma.igwAttach.res.internetGatewayId). Routes should reference the gateway ID through the attachment, not the gateway directly, to ensure correct destroy ordering. Without this, destroying a stack with Routes and an IGW attachment could hang for hours because formae tried to detach the IGW before deleting the routes that use it.listener.res.targetGroupArn, resolving to the target-group ARN attached via the listener's first default action. ECS Services (and any other consumer that needs the target group already wired to the load balancer) should reference this instead of tg.res.targetGroupArn. Without it, AWS rejects ECS service creation with "target group does not have an associated load balancer" when formae schedules the service before the listener attaches the TG. Same pattern as igwAttach.res.internetGatewayId for routes/IGW.EventInvokeConfig.DestinationConfig) could fail with CloudControl errors like required key [Destination] not found when the nested object was empty. The plugin now strips empty sub-objects from replace operations the same way it already did for add operations, so these updates succeed.awsvpc tasks that the user didn't set, which previously made the planner think the task definition had changed. Those fields are now recognised as provider-populated and ignored during comparison. Requires formae 0.84.0.EFSVolumeConfiguration.rootDirectory no longer causes phantom replacements. CloudControl returns RootDirectory as "/" even when it was never set, which made the planner see a change on every reapply.createOnly field and triggered a full replace. ARN-vs-short-name differences are now normalised before comparison.example.com.-SOA and example.com.-NS.HealthCheckConfig and AlarmIdentifier sub-resources, enabling richer health check definitions in Pkl.ResourceLifecycleConfig fields to ElasticBeanstalk environments and configuration templates.apprunner/service.pkl to apprunner/apprunnerservice.pkl for consistency with the naming convention used across other resource schemas.ListResults, preventing unrelated resources from appearing in extracted Pkl output.AWS::AppRunner::Service), enabling management of AppRunner web services through formae.af-south-1 availability zone pattern. The Region typealias already included af-south-1, but the AvailabilityZone constraint was missing it, causing Pkl evaluation failures for resources in that region.