> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formae.io/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK release notes

> Release notes for the formae Plugin SDK packages: pkg/plugin, the conformance-test framework, and the plugin template.

This page tracks releases of the formae Plugin SDK packages:

* **pkg/plugin** - Core SDK for building plugins
* **pkg/plugin-conformance-tests** - Conformance test framework
* **[formae-plugin-template](https://github.com/platform-engineering-labs/formae-plugin-template)** - Scaffold for new plugins

***

## July 2026

### formae schema 0.88.0

*Released July 2026. Requires formae ≥0.88.0.*

**Feature**: `formae.SecretValue` for secret fields

Mark a resource field as a secret by typing it `(String | formae.SecretValue)` (nullable and union variants included). formae derives the field's opaque hint from the type, so the agent stores the value hashed at rest and keeps it out of logs. This is independent of `writeOnly`, which controls read-back and drift; a secret the provider never returns is typically both.

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint {}
masterPassword: (String|formae.SecretValue)?
```

### plugin v0.4.1

*Released July 2026. Requires formae ≥0.87.1 for `PriorProperties` to be populated.*

**Feature**: `PriorProperties` on `ReadRequest`

`ReadRequest` now carries an optional `PriorProperties` field holding the caller's last-known model for the resource (the stored row). A plugin's `Read` can consult it to report the projection the caller actually manages. For example, it can embed a child collection only when the prior model declares it inline, so a caller that manages those children as standalone resources is not shown phantom drift or driven to destroy them on a reconcile.

The field is optional and additive: a plugin that ignores it behaves exactly as before, and no migration is required. It is empty when the caller has no prior state, such as a create/status read-back or discovery, so treat empty as "unknown" and fall back to your default behaviour. The agent populates it starting formae 0.87.1; on older agents it is always empty.

```go theme={"languages":{"custom":["/languages/pkl.json"]}}
func (r *Role) Read(ctx context.Context, req *resource.ReadRequest) (*resource.ReadResult, error) {
    // ... read actual cloud state into props ...
    if callerManagesPoliciesInline(req.PriorProperties) {
        props["Policies"] = inlinePolicies
    }
    // ... marshal and return ...
}
```

`PriorProperties` is a `json.RawMessage`; unmarshal it to inspect the caller's model. A read still reports actual cloud state. This hint only disambiguates a resource's boundary where the provider conflates it with its children.

## June 2026

### plugin v0.4.0

*Released June 2026. Requires formae ≥0.87.0*

**Breaking behaviour.** The `writeOnly` field hint no longer implies "re-send on every update." `writeOnly` now means only that the provider never returns the value on read, so it is stripped from read-back state and never produces drift. The "the provider drops this value unless it is included in every update" semantics that `writeOnly` previously bundled in now live in a separate hint, `requiredOnUpdate`.

**Migration.** Audit every field marked `@formae.FieldHint { writeOnly = true }`. If the provider *requires* the value to be present on every update (typical of rotating secrets and write-only configuration blocks the API replaces wholesale), add `requiredOnUpdate`, or the value silently stops being sent and the next update drops it:

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { writeOnly = true; requiredOnUpdate = true }
clientSecret: String?
```

A field the provider retains across updates without re-sending needs no change: it stays `writeOnly = true` and is no longer needlessly re-sent when an unrelated field on the same resource changes. Run your conformance suite's update phase: a field the provider drops will surface as a post-update diff, which is the signal it needs `requiredOnUpdate`.

**Feature**: `format` on `FieldHint` for serialized-content String fields

A String field can now declare the structured format of its content with `@formae.FieldHint { format = "json" }`. When set, the agent compares the field by its canonical content rather than its raw byte serialization: differences that are purely cosmetic (object key ordering and insignificant whitespace) no longer register as a change, so they neither surface as drift on sync nor produce a no-op update on apply.

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { format = "json" }
dashboard: String
```

The motivating case is a field populated from a pretty-printed source document, for example a Grafana dashboard loaded with `read("dashboard.json")`. The provider stores the JSON in its own canonical (often minified, key-reordered) form, so on the next read the bytes differ from the source even though the document is identical. With `format = "json"` the two are compared as equal JSON documents and the spurious diff disappears.

`format` defaults to `""`, which leaves the field an opaque String compared byte-for-byte (unchanged behaviour). `"json"` is the only format recognised today; an unknown value is rejected when the plugin registers rather than silently ignored, and a JSON document with duplicate object keys is rejected.

### plugin v0.3.0

*Released June 2026. Requires formae ≥0.86.0*

**Breaking behaviour.** `ExtractSchema` now emits every property path, including unannotated ones, with `createOnly` defaulting to `false`. The planner uses this complete schema surface to decide between cascade-replace and cascade-update when a parent resource is being replaced. Any field whose value the provider rejects on update must now be annotated `@formae.FieldHint { createOnly = true }`. Previously the planner could not distinguish "unannotated" from "mutable" and cascade-replaced dependents wholesale; now an unannotated dependent field is treated as mutable and the resolvable re-resolves at apply time, so the dependent absorbs the new parent value through a provider-native Update. Plugins that under-mark immutable fields will see apply-time provider rejections that previously masqueraded as cascade-replaces.

The canonical case this unlocks is an ECS Service consuming a TaskDefinition via the mutable `taskDefinition` field: previously every revision bump tore down and recreated the Service (2–4 minutes of outage); with `taskDefinition` correctly unmarked (i.e. mutable), the Service does a rolling deploy.

**Migration**: audit your schema for fields whose value the provider rejects on update (typical: a resource's name/identifier, region, KMS key ID at create time, networking primitives). Add `@formae.FieldHint { createOnly = true }` to each. Run your conformance suite; the update phase now exercises the planner's mutability decision end-to-end.

**Resolvables follow the same rule.** Cross-resource references (fields whose values are `Resolvable` URIs pointing at other resources) were previously treated as implicitly `createOnly`. They now follow the same rule as scalar fields: a `Resolvable`-typed field is treated as immutable only when annotated `@formae.FieldHint { createOnly = true }`. Plugins where a resolvable-bearing field's value can be safely updated in place by the provider should leave it unannotated; plugins where changing the reference requires a replace should mark it explicitly. The same audit applies: review every `Resolvable`-typed field and add the annotation where the provider rejects in-place updates.

**Feature**: Typed `EdgeKind` on `FieldHint` replaces the `AttachesTo` boolean

`FieldHint` now carries an `edgeKind` union with three values: `default`, `attachesTo`, and the new `runtimeDependency`. The legacy `attachesTo = true` boolean is still accepted and derived to `edgeKind = "attachesTo"` for one release, so existing plugins compiled against `v0.2.2` continue to load unchanged. New plugins should write `edgeKind` directly:

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { edgeKind = "runtimeDependency" }
fileSystemId: (String|formae.Resolvable)?
```

**Feature**: `runtimeDependency` edge kind for instance-scoped destroy ordering

`runtimeDependency` annotates a field whose value points at a specific resource *instance* that must outlive its containment children during destroy. The planner inserts edges from the annotated consumer to each containment child that points at the same producer instance, so the children are destroyed before their parent.

The motivating case is `AWS::EFS::FileSystem` + `AWS::EFS::MountTarget`: an ECS TaskDefinition references a FileSystem via a mutable field, and the FileSystem has MountTargets as containment children. Without this annotation, the engine could delete the FileSystem while MountTargets still held it open, stranding both. Annotating the TaskDefinition's `fileSystemId` field with `edgeKind = "runtimeDependency"` pulls those MountTargets into the destroy ordering ahead of their parent FileSystem.

`attachesTo` (the same semantics as the previous boolean) still flips the destroy edge between two specific resources where the attacher must outlive the host. `runtimeDependency` is the multi-instance, containment-aware variant.

**Feature**: `ParentRef` and `Schema.ParentMappings` describe parent-instance identity

A new `ParentRef` PKL class and `ResourceHint.parentRefs` field let resource types declare which fields identify their parent instance. `Schema.Parent` and `Schema.ParentMappings` are now plumbed from PKL through the descriptor extractor and are available to the planner for instance-discriminated child-points-at lookups during destroy. This is what makes `runtimeDependency` instance-safe rather than type-wide.

**Fix**: `formae plugin init` scaffolder: namespace, substitution, and verify-schema glob

Three rough edges surfaced by the atlas plugin scaffold are fixed:

* Mixed-case namespace input is now normalised to upper-case at input time, with a one-line stderr notice when the input differs from the normalised form. Previously the conversion happened silently at template-generation time and left `config.Namespace` mixed-case internally.
* Five additional template-substitution rules cover placeholders that were leaking through scaffolded output: bare `module example` declarations, PklProject `baseUri` and `packageZipUrl` strings, PKL module-qualifier prefixes (`example.ExampleResource`), the license header year and author (previously hard-coded to 2025), and the new `@example/core/example.pkl` subdir import path that pairs with the template repo's subdir convention.
* The `testutil` `ImportsGenerator.pkl` glob is changed from `import*(@<ns>/**/*.pkl)` to the union pattern already used in the sibling generators under `internal/schema/pkl/generator` and `pkg/plugin/descriptors`. The single-glob form misses top-level files in PKL's glob model, so `make verify-schema` was reporting "Total resource types: 0, PASSED" for plugins that placed resources at `schema/pkl/<resource>.pkl`: a false-success signal that hid real schema problems. The new pattern matches both top-level and nested files.

**Docs**: Package READMEs

`pkg/plugin/README.md` and `pkg/plugin-conformance-tests/README.md` are added so plugin authors who land in those directories directly (via `go doc`, GitHub's package tree, or as a follow-up to `formae plugin init`) get a self-contained orientation. The plugin README covers the `ResourcePlugin` interface, the optional `ObservablePlugin` and `Configurable` interfaces, the async `ProgressResult` flow, the on-disk layout (`formae-plugin.pkl` manifest plus `schema/pkl/PklProject`), the `sdk.RunWithManifest` entry point, and the conventions plugin authors commonly miss (statelessness across operations, mandatory `NativeID` in every `ProgressResult`, JSON-typed properties, no pointer sharing across the actor boundary).

**Migration from v0.2.2**: Bump the dependency and rebuild:

```
github.com/platform-engineering-labs/formae/pkg/plugin v0.3.0
github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.5
github.com/platform-engineering-labs/formae/pkg/model v0.1.25
```

Then `go mod tidy` and `make build`. Bump `minFormaeVersion` in `formae-plugin.pkl` to `0.86.0` if you plan to ship any `edgeKind` or `parentRefs` annotations.

***

### conformance-tests v0.2.5

*Released June 2026. Requires plugin ≥0.3.0*

**Feature**: Channel-aware formae resolver with dev fallback

The harness now reads `minFormaeVersion` from your plugin's `formae-plugin.pkl` manifest and uses it as the floor when picking a formae release to install via orbital. It prefers the highest stable release that meets the floor; otherwise it falls back to the highest qualifying release on the dev channel; otherwise it errors with a message naming the required minimum.

This lets a plugin opt into a not-yet-stable formae release by declaring a higher minimum in its manifest. For example, a plugin adopting the new `edgeKind` schema needs formae ≥0.86.0; while 0.86.0 is in its dev-channel window, the harness picks the latest qualifying dev build automatically. Plugins whose declared minimum is satisfied by the latest stable release see no behaviour change.

Each channel uses its own orbital tree under the conformance tempdir, and the dev tree is only initialised when stable cannot satisfy the floor. An explicit `FORMAE_VERSION` (used by nightly and `debug-conformance`) is still honoured exactly, now searched across stable then dev.

**Fix**: Empty-value drift no longer flagged in the reverse comparison direction

The forward direction of `compareMap` and `compareProperties` skips when the expected value is structurally absent (nil, empty array, or empty map). The reverse direction had no equivalent forgiveness: any actual key absent from expected was flagged as drift unless the schema marked the field as `hasProviderDefault`. Cloud providers commonly return `[]` for unset list fields and `{}` for unset map fields, which is semantically equivalent to the key being omitted, but the harness was flagging it.

This forced plugin authors into a bad trade-off for user-canonical fields. Marking `hasProviderDefault` on the schema satisfied the harness, but formae core's strip-provider-default pass then dropped the field symmetrically from apply diffs, silently losing user changes. Setting explicit empty literals in testdata lost test-coverage clarity and bloated fixtures.

The reverse loops now mirror the forward loops' empty-skip. Non-empty unexpected values are still flagged, which is the case that matters for both out-of-band changes and missed `hasProviderDefault` annotations. This unblocks `AWS::ECS::TaskDefinition` (18 `ContainerDefinitions[0].*` fields plus `Tags`), `AWS::ECS::TaskSet.Tags`, and `AWS::SES::EmailIdentity.Tags`, among others.

**Fix**: `FORMAE_VERSION` is now populated when `FORMAE_BINARY` is set externally

`EnsureFormaeBinary` returned early when `FORMAE_BINARY` was already set in the environment, without extracting the version or populating `FORMAE_VERSION`. Downstream, `ResolvePKLDependencies` saw an empty `FORMAE_VERSION`, logged "skipping PKL dependency resolution", and the test's subsequent `pkl eval` failed with `NoSuchFileException` on `testdata/PklProject.deps.json`. That file is gitignored and only materialises once `pkl project resolve` has run, so every nightly that built formae from source regressed in lockstep once the per-plugin `run-conformance-tests.sh` wrapper was retired in favour of letting the SDK manage setup. The externally-set branch now mirrors the orbital path: extract the version, log it, and `t.Setenv FORMAE_VERSION` before returning.

**Migration from v0.2.4**: Bump the dependency:

```
github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.5
```

Then `go mod tidy`. No code changes required. If you want to opt your plugin into a dev-channel formae release, raise `minFormaeVersion` in `formae-plugin.pkl`.

***

### Full go.mod bump for this cycle

```
github.com/platform-engineering-labs/formae/pkg/plugin v0.3.0
github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.5
github.com/platform-engineering-labs/formae/pkg/model v0.1.25
```

Run `go mod tidy` and `make build` after updating. Bump `minFormaeVersion` in `formae-plugin.pkl` to `0.86.0` if your plugin adopts the new `edgeKind` or `parentRefs` annotations; otherwise the existing `0.84.0` floor still works.

***

## May 2026

### plugin v0.2.2

*Released May 13, 2026. Requires formae ≥0.84.0*

**Feature**: `AttachesTo` field hint for reachability-based destroy ordering

Plugin authors can now annotate a field that `$ref`s another resource with `attachesTo = true` to declare that the resource being defined attaches as a backend to the resource it references, and must outlive it during destroy. The destroy DAG inverts the edge: the referenced resource (the host) is deleted first, and the attaching resource waits.

**When to use it.** Annotate a field with `attachesTo` when the resource being defined provides a live capability (a serving endpoint, a consumer connection) that depends on the continued existence of the resource it references, and when other resources in the stack consume that live capability during their own tear-down. The canonical example is `AWS::ECS::Service.LoadBalancers.targetGroupArn`: the ECS service is the live backend for the listener URL. A Grafana target pointing at that listener URL drives CRUD calls through it during destroy; if the service tears down in parallel with the listener, those calls fail and the agent wedges mid-destroy. With `attachesTo`, the service waits for the listener to be deleted first, keeping the HTTP endpoint reachable throughout the Grafana resources' tear-down.

**DAG semantics.** This is a destroy-only annotation. Create order is unchanged: the normal construction constraint still applies (the attacher is created after its host). On destroy, instead of the default reverse-construction order (consumer before producer), an `attachesTo` edge flips direction: the host is destroyed first, then the attacher.

**PKL syntax:**

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { attachesTo = true }
targetGroupArn: (String|formae.Resolvable)?
```

**Compatibility.** The `AttachesTo` field defaults to `false` on JSON unmarshal of stored resources whose schemas were written before this release. Existing plugins compiled against `pkg/plugin@v0.2.1` or earlier continue to load unchanged. Agents older than `0.84.0` silently ignore the annotation (the field round-trips as `false`).

**Migration from v0.2.1:** Bump the dependency and rebuild:

```
github.com/platform-engineering-labs/formae/pkg/plugin v0.2.2
github.com/platform-engineering-labs/formae/pkg/model v0.1.24
```

Then `go mod tidy` and `make build`. Plugins that do not adopt `attachesTo` need no code changes.

***

### conformance-tests v0.2.4

*Released May 13, 2026. Requires plugin ≥0.2.2*

**Feature**: `--schema-location local` for unpublished plugins

The conformance harness now passes `--schema-location local` to `formae extract` during conformance runs. Plugins whose schema has not yet been published to Hub no longer need a Hub entry to run their conformance suite; the agent resolves the schema from the locally loaded plugin binary instead.

**Feature**: `FORMAE_TEST_TESTDATA_DIR` override

Set `FORMAE_TEST_TESTDATA_DIR` to point at an alternate fixture directory at runtime. This lets plugin authors test against different fixture sets (for example, region-specific resources or staged migration fixtures) without modifying test code or Makefile targets.

**Migration from v0.2.3:** Bump the dependency:

```
github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.4
```

Then `go mod tidy`. No code changes required.

***

### Full go.mod bump for this cycle

```
github.com/platform-engineering-labs/formae/pkg/plugin v0.2.2
github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.4
github.com/platform-engineering-labs/formae/pkg/model v0.1.24
```

Run `go mod tidy` and `make build` after updating. The Makefile's auto-derivation keeps `minFormaeVersion` in `formae-plugin.pkl` at `0.84.0`; no behavioural change for plugins that do not adopt `attachesTo`.

***

## April 2026

### conformance-tests v0.2.3

*Released April 27, 2026. Requires plugin ≥0.2.1*

**Feature**: Configurable timeouts for the OOB-delete CRUD phase and the OOB plugin RPCs

Two timeouts that previously fired in the middle of healthy long-running operations on slow backends are now configurable, and one of them has a much higher default. Slow managed-Kubernetes resources (`AWS::EKS::Cluster`, `OVH::Kube::Cluster`, etc.) were the main motivator: they were failing conformance not because of bugs, but because the harness gave up before the cloud API finished.

| Env var                          | Bounds                                                                                                                                                                                                          | Default                                             | Override when                                                                                                         |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `FORMAE_TEST_OOB_TIMEOUT`        | The OOB Create / OOB Delete plugin RPC waits inside `retryOnRecoverable` (single attempt of `waitForOperationProgress`). Used by both the discovery test's CreateOOB step and the CRUD test's OOB-delete setup. | **30 minutes** (raised from a hardcoded 10 minutes) | A single Create/Delete RPC to your plugin can legitimately take longer than 30 minutes. Most plugins won't need this. |
| `FORMAE_TEST_OOB_DELETE_TIMEOUT` | Step 24 of the CRUD test: the post-sync inventory-tombstone wait, *after* the plugin's Delete RPC has already returned.                                                                                         | **2 minutes** (unchanged; was previously hardcoded) | Sync → tombstone propagation in your agent setup is slow.                                                             |

Both default to values that match what we currently exercise on AWS / OVH; raise them only if you observe timeouts on a specific resource, with a value in **integer minutes** (matches the `FORMAE_TEST_TIMEOUT` and `FORMAE_TEST_DISCOVERY_TIMEOUT` convention).

The two knobs are independent: they bound different waits in different phases and can be set independently in CI.

Pairs with the AWS plugin fixes in [formae-plugin-aws#47](https://github.com/platform-engineering-labs/formae-plugin-aws/pull/47), which gets `eks-cluster` off the timeout list.

**Migration from v0.2.2**: Bump the dependency:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
go get github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests@v0.2.3
go mod tidy
```

No code changes required. Existing conformance test suites continue to work unchanged; the raised default for `FORMAE_TEST_OOB_TIMEOUT` only affects how long the harness is willing to wait for slow-provisioning resources before failing.

***

### plugin v0.2.1

*Released April 18, 2026. Requires formae ≥0.84.0*

**Feature**: Plugin compatibility checks

Plugins now verify agent compatibility at startup. If the agent version is older
than the SDK's minimum requirement, the plugin exits with a clear error message
instead of failing with cryptic wire protocol errors. The agent also checks
plugin compatibility during discovery and skips incompatible plugins with
actionable warnings.

The SDK exports two constants, `MinFormaeVersion` and `SDKVersion`, that
plugins can use in build tooling. The `make build` target in the
[plugin template](https://github.com/platform-engineering-labs/formae-plugin-template)
now auto-generates `minFormaeVersion` in the manifest from the SDK constant,
removing the need for plugin authors to set it manually.

**Feature**: Conformance test harness handles binary download

The conformance test harness now downloads the formae binary automatically via
the orbital package manager when `FORMAE_BINARY` is not set. PKL dependency
resolution is also handled by the harness. Plugin authors no longer need the
`scripts/run-conformance-tests.sh` shell script; the Makefile conformance
targets call `go test -tags=conformance` directly. Future changes to the
download mechanism or PKL resolution will flow through `go get` without
requiring manual script updates.

**Migration from v0.2.0**: Update your `go.mod`:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
go get github.com/platform-engineering-labs/formae/pkg/plugin@v0.2.1
go get github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests@v0.2.1
go mod tidy
```

Optionally update your Makefile to match the
[template](https://github.com/platform-engineering-labs/formae-plugin-template/blob/main/Makefile)
(simplified conformance targets, broadened schema install, auto-generated
`minFormaeVersion`) and delete `scripts/run-conformance-tests.sh`.

***

### plugin v0.2.0

*Released April 16, 2026. Requires formae ≥0.84.0 and Go 1.26+*

**Breaking change.** The wire protocol between the agent and plugins has changed from EDF struct encoding to MessagePack. **All plugins must be recompiled** against this version. Plugins built against older SDK versions will fail with message decoding errors.

**Change**: MessagePack serialization replaces EDF

The internal wire protocol between the agent and plugins now uses MessagePack
with zstd compression instead of Ergo's native EDF struct encoding. This is
a foundational change that enables backwards-compatible schema evolution:
adding new fields to SDK types no longer breaks existing plugins. Previously,
any field addition to a message type required all plugins to be recompiled
simultaneously.

Plugin developers do not need to handle serialization directly; it is
transparent. The `CompressResource()` and `CompressJSON()` helpers from v0.1.21
are no longer needed and have been removed; compression is now handled at the
protocol layer.

**Change**: Simplified EDF type registration

`RegisterSharedEDFTypes()` now registers 13 top-level message types instead of
the \~40 dependency-ordered types required previously. Nested types are handled
by MessagePack, eliminating the registration ordering issues that plugin
authors occasionally hit when adding new message types.

**Feature**: `Configurable` interface for plugin-specific configuration

Plugins can now receive custom configuration from the user's `formae.conf.pkl`.
Implement the optional `Configurable` interface to accept plugin-specific
settings beyond the standard rate limits and discovery filters:

```go theme={"languages":{"custom":["/languages/pkl.json"]}}
type Configurable interface {
    Configure(config json.RawMessage) error
}
```

Define your configuration schema in `schema/Config.pkl` extending
`BaseResourcePluginConfig`, and users can set plugin-specific fields in the
`agent.resourcePlugins` block of their configuration. See

**Change**: Configuration types moved to `pkg/model`

`RateLimitConfig`, `MatchFilter`, `FilterCondition`, and `LabelConfig` have
moved from `pkg/plugin` to `pkg/model`. Update your imports accordingly:

```go theme={"languages":{"custom":["/languages/pkl.json"]}}
// Before
plugin.RateLimitConfig{...}

// After
import pkgmodel "github.com/platform-engineering-labs/formae/pkg/model"
pkgmodel.RateLimitConfig{...}
```

**Feature**: Discovery filters and label configuration

Two new methods on the `ResourcePlugin` interface let plugins declaratively
shape discovery behavior:

* `DiscoveryFilters()`: return filters to exclude specific resources from
  discovery (e.g., Kubernetes-owned resources in AWS)
* `LabelConfig()`: configure how human-readable labels are extracted from
  discovered resources using JSONPath queries

Both methods are required. Plugins that don't need filtering or custom labels
should return `nil` and `pkgmodel.LabelConfig{}` respectively.

**Change**: Ergo framework upgraded to v3.2.0

The Ergo actor framework has been upgraded from v3.1.0 to v3.2.0. This is
handled transparently by the SDK; plugin developers do not need to interact
with Ergo directly.

**Fix**: Ergo banner no longer printed in plugin processes

Plugin processes no longer print the Ergo Framework ASCII banner on startup,
keeping plugin logs clean.

**Fix**: Stack overflow on self-referencing sub-resource types

`reverseSubResourceFieldMapping` now tracks visited classes and breaks the
recursion on self-referencing types (for example, BigQuery's `SchemaField`).
Extract and PKL generation no longer hit a stack overflow when plugins expose
sub-resources that recurse into their own type.

***

### conformance-tests v0.2.2

*Released April 18, 2026. Requires plugin ≥0.2.1*

**Fix**: Correct orbital hub URL and version extraction

Fixed the hub URL used for downloading formae (was pointing to the old
`binaries/repo.json` endpoint instead of the orbital repository). Also fixed
version extraction to parse the formae version from `--version` output correctly.

***

### conformance-tests v0.2.1

*Released April 18, 2026. Requires plugin ≥0.2.1*

**Feature**: Built-in binary download via orbital

The harness now downloads the formae binary automatically when `FORMAE_BINARY`
is not set, using the same orbital package manager that powers `formae update`.
This eliminates the need for `scripts/run-conformance-tests.sh`. PKL dependency
resolution (version pinning + `pkl project resolve`) is also handled internally.

**Improvement**: Graceful handling of missing PklProject files

`ResolvePKLDependencies` now skips directories without a `PklProject` file
instead of failing, supporting minimal plugins that may not have
`testdata/PklProject`.

***

### conformance-tests v0.2.0

*Released April 16, 2026. Requires plugin ≥0.2.0 and Go 1.26+*

**Feature**: Per-plugin typed configuration in test harness

The test harness now generates configuration using typed plugin imports
(`import "plugins:/<Plugin>.pkl"`) and per-plugin `resourcePlugins` blocks,
matching the new configuration format in formae ≥0.84.0. Plugins must have a
`schema/Config.pkl` extending `BaseResourcePluginConfig` for the harness to
generate correct configuration.

**Feature**: Configurable discovery resource types via environment variable

The `FORMAE_DISCOVERY_TYPES` environment variable allows overriding which
resource types the harness configures for discovery tests, without modifying
test code.

**Feature**: Step-level result matrix in test output

Conformance test output now includes per-phase outcomes via the new
`ResultCollector`: 8 CRUD phases (Create, Verify, Extract, Sync, Update,
Replace, Destroy, OOB Delete) and 4 discovery phases (CreateOOB, Register,
Discover, Verify). This makes it easy to see at a glance which phase failed
instead of parsing the full test log.

**Change**: Top-level `pluginDir` in generated configuration

The test configuration now includes `pluginDir` at the top level, matching the
configuration schema change in formae ≥0.84.0.

**Fix**: Discovery NativeID matching for charts with multiple resource types

The discovery test previously took the NativeID from the last-created resource
and the resource type from the last resource in the PKL output, which could
differ when dependency order and PKL output order diverged (for example, a
cert-manager chart whose last PKL resource is a Service but last-created
resource is a Deployment). The harness now searches backwards through created
resources for the one matching the extracted type, so discovery tests for
multi-resource charts no longer wait for a non-existent combination.

***

## March 2026

### plugin v0.1.21

*Released March 28, 2026*

**Breaking change.** The internal message format between the agent and plugins has changed. **All plugins must be recompiled** against this version. Plugins built against older SDK versions will fail with message decoding errors during synchronization and resource operations.

**Change**: Compressed plugin messages

Resource fields in all plugin operation messages (`ReadResource`,
`CreateResource`, `UpdateResource`, `DeleteResource`) are now
gzip-compressed for transport. This resolves an issue where resources with
large schemas (e.g. Kubernetes Pod, AWS ECS TaskDefinition) could exceed
internal transport limits, causing silent failures during synchronization
and updates.

Plugin developers do not need to handle compression; it is transparent.
If you construct `ReadResource` or `DeleteResource` directly (e.g. in
test utilities), use `plugin.CompressResource()` for the `Resource` field
and `plugin.CompressJSON()` for property fields.

**Fix**: Plugin error messages now included in failure responses

When a plugin's CRUD method returns an error, the error text is now
included in the response. Previously the message was empty, making
failures difficult to diagnose from the CLI or API.

***

### conformance-tests v0.1.40

*Released March 28, 2026*

**Feature**: Out-of-band delete detection test

The CRUD lifecycle now verifies that the agent correctly detects when a
resource is deleted outside of formae.
After the standard destroy cycle, the test recreates the resource, deletes
it directly via the plugin (bypassing formae),
triggers a sync, and verifies the resource is removed from inventory.

**Feature**: Collection-aware `hasProviderDefault` for Mapping fields

Property comparison now correctly handles `hasProviderDefault` on
Mapping-type fields, preventing false test failures when cloud providers
populate extra keys in map fields.

***

### conformance-tests v0.1.25

*Released March 17, 2026*

**Feature**: Regex support for test filter

The `TEST=` parameter now supports `/…/` delimited regular expressions for
flexible test group selection. For example, `TEST='/.*-chart/'` selects all chart
tests and `TEST='/^cluster.*/'` selects all cluster-scoped resources. Regex and
literal segments can be mixed in a single comma-separated filter (e.g.,
`TEST='/.*-chart/,namespace'`). Invalid regex patterns cause immediate failure
with a clear error message. See
[Filtering Tests](/plugin-development/tutorial/11-conformance#filtering-tests) for details.

**Feature**: Configurable discovery timeout

The discovery test timeout is now configurable via the `FORMAE_TEST_DISCOVERY_TIMEOUT`
environment variable (in minutes). Previously hardcoded to 2 minutes, this can now be
increased for plugins with many resource types or chart-based test cases that need more
time for discovery to complete. See
[Environment Variables](/plugin-development/tutorial/11-conformance#environment-variables) for details.

***

### conformance-tests v0.1.24

*Released March 12, 2026*

**Fix**: Retry create/delete operations on recoverable errors

The test harness now retries `CreateResource` and `DeleteResource` operations
when the plugin returns a recoverable error, instead of failing the test
immediately. This reduces flaky test failures caused by transient cloud provider
errors during conformance runs.

***

### conformance-tests v0.1.23

*Released March 10, 2026*

**Fix**: Honor `@HasProviderDefault` annotation in property comparison

Previously, conformance tests ignored extra fields returned by plugins, which
could hide issues where plugins return unexpected data. Tests now validate that
extra fields in the actual state are only allowed if marked with
[`@HasProviderDefault`](/plugin-development/reference/schema#hasproviderdefault) in the schema.
Unexpected extra fields will now correctly fail the test.

***

### conformance-tests v0.1.22

*Released March 9, 2026*

**Fix**: Property comparison for Listing fields containing resolvables

Previously, conformance tests would fail for schemas with `Listing` fields where
elements contained resolvable references (e.g., `subjects: Listing<Subject>` with
a resolvable `namespace` property inside `Subject`). The test framework now
correctly compares these nested resolvables.

***

## February 2026

### plugin v0.1.14

*Released February 23, 2026. Requires formae ≥0.82.2*

**Fix**: Transitive `pkg/model` dependency

Fixed incorrect transitive dependency on `pkg/model` that caused EDF
serialization errors. The `pkg/model` dependency is now pinned to v0.1.5 across
all SDK modules.

***

### conformance-tests v0.1.21

*Released February 23, 2026. Requires plugin ≥0.1.14*

**Fix**: Transitive `pkg/model` dependency

Bumped `pkg/model` to v0.1.5 to match `plugin v0.1.14`, fixing EDF
serialization errors caused by mismatched transitive dependencies.

***

### plugin-template

**Feature**: `verify-schema` CI job

Added a `verify-schema` CI job to the template that validates PKL schemas can be
generated correctly, catching schema errors before they reach plugin releases.

***

### conformance-tests v0.1.20

*Released February 23, 2026*

**Fix**: Nested Resolvables in SubResource map comparison

`compareProperties()` used flat string comparison for non-Resolvable,
non-array values. When a SubResource map (e.g., `ResourceLifecycleConfig`)
contained a nested Resolvable reference, the entire map was string-compared
`$visibility:Clear` from Pkl eval vs `$value:arn:...` from resolved inventory,
causing false test failures.

Added recursive `compareMap()` that delegates to `compareResolvable()` for
nested Resolvables, `compareArrayUnordered()` for arrays, and recurses into
sub-maps.

***

### plugin v0.1.13

*Requires formae ≥0.82.0*

**Feature**: HasProviderDefault field hint

New `hasProviderDefault` annotation for fields where cloud providers assign
default values (e.g., S3 bucket encryption, SQS visibility timeout).

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
@formae.FieldHint { hasProviderDefault = true }
bucketEncryption: BucketEncryption?
```

When a field with this hint exists in actual state but is not specified by the
user, formae accepts the provider's default instead of generating a "remove"
operation. This prevents oscillation during reconcile cycles.

See the [schema reference](/plugin-development/reference/schema#hasproviderdefault) for details.

***

### conformance-tests v0.1.19

*Released February 10, 2026*

**Improvement**: Robust property comparison for arrays, resolvables, and string escaping

This release significantly improves how the conformance test framework compares
expected vs actual resource properties:

* **Order-independent array comparison**: All array properties are now compared
  as unordered sets. Elements are serialized to JSON and sorted before
  comparison, eliminating false failures caused by CloudControl returning arrays
  in a different order than specified in PKL.

* **Resolvable handling inside arrays**: Arrays containing resolvable
  references (e.g., Tags with resolved values) are now matched element-wise
  using metadata keys (`$label`, `$type`, `$stack`, `$property`). Resolvables
  are handled gracefully after extraction, where `$value` may no longer be
  present.

* **String escaping normalization**: JSON/PKL round-trip differences in
  backslash escaping no longer cause false mismatches. Extraction comparison
  now reuses the same comparison logic as inventory comparison, benefiting from
  all the above improvements.

***

### plugin v0.1.12

*Released February 6, 2026*

**Fix**: pkl-go race condition

Fixed a race condition in pkl-go's `NewProjectEvaluator` where a late message
from the pkl subprocess could kill the evaluator manager's listen loop, causing
evaluation failures. This primarily affected CI environments under resource
pressure.

***

### plugin v0.1.11

*Released February 5, 2026*

**Feature**: Plugin telemetry metrics

Plugins now export Ergo actor framework metrics via OpenTelemetry, providing
visibility into plugin process health and performance.

***

### conformance-tests v0.1.14

*Released February 4, 2026*

**Feature**: Configurable test timeout

Override the default 5-minute timeout for resources that take longer to
provision:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
make conformance-test TIMEOUT=900
```

**Note:** Requires updating your Makefile from the
[plugin template repository](https://github.com/platform-engineering-labs/formae-plugin-template).

***

**Feature**: Config directory support

You can now create a `config/` directory under `testdata/` containing shared Pkl
code (such as targets or stacks). Files in this directory are not picked up as
test cases by the conformance test framework.

***

## January 2026

### conformance-tests v0.1.13

*Released January 31, 2026. Requires plugin v0.1.9.*

**Feature**: Parallel test execution with configurable Ergo ports

Run conformance tests in parallel to significantly reduce test execution time:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
make conformance-test PARALLEL=10
```

When running parallel tests, multiple formae agent instances need unique ports
for Ergo node communication. This release adds `ergoPort` configuration to
prevent port conflicts.

**Note:** Requires updating your Makefile from the
[plugin template repository](https://github.com/platform-engineering-labs/formae-plugin-template).

***

### conformance-tests v0.1.12

*Released January 30, 2026*

**Feature**: Parallel test execution support

Added infrastructure for running conformance tests in parallel via
`FORMAE_TEST_PARALLEL` environment variable.

***

### conformance-tests v0.1.10

*Released January 30, 2026*

**Conformance test improvements**

* Faster and more reliable discovery tests in CI environments
* Extract validation step now runs correctly for all extractable resources
* Improved error messages when validations fail

***

### plugin v0.1.9

*Released January 31, 2026*

**Feature**: Configurable Ergo port

Plugins now read `FORMAE_ERGO_PORT` environment variable to configure Ergo
acceptors, enabling parallel test execution.

***

### plugin v0.1.8

*Released January 30, 2026*

**Fix**: Plugin log levels now preserved

Plugin logs were incorrectly routed through stderr, causing the formae agent to
treat all plugin logs as errors regardless of their actual log level. Logs now
route through stdout with correct log levels preserved.
