diff --git a/docs/README.md b/docs/README.md
index f680609..cedbf42 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -66,6 +66,6 @@ exposes:
- **[Concepts](/fluid/concepts/)** — the agentic-native layer and how FLUID compares to ODCS / ODPS.
- **[Schema Reference](/fluid/schema/anatomy)** — every top-level block, a cheatsheet, and the full specification.
- **[What's New in 0.7.5](/fluid/releases/0.7.5)** — the latest stable version: streaming Kafka → Iceberg sink, Confluent Cloud Tableflow, and a pgvector output port.
-- **[0.7.6 (preview)](/fluid/releases/0.7.6)** — packaging modes, declared consumers, cross-mesh pins; opt-in, and still subject to change.
+- **[0.7.6 (preview)](/fluid/releases/0.7.6)** — packaging modes, declared consumers, cross-mesh pins, row filters and masked columns, Flink builds; opt-in, and still subject to change.
- **[See the deck](/fluid/deck/)** — the FLUID story in slides.
- **[Contributing & governance](/fluid/contributing/)** — how the standard is maintained, and how to take part.
diff --git a/docs/releases/0.7.6.md b/docs/releases/0.7.6.md
index fd6187e..bbd8b23 100644
--- a/docs/releases/0.7.6.md
+++ b/docs/releases/0.7.6.md
@@ -4,7 +4,7 @@
0.7.6 is a **preview**. A contract uses it only by declaring `fluidVersion: "0.7.6"`, and its fields and their meaning can still change before it is promoted to stable. The latest stable version is [0.7.5](/fluid/releases/0.7.5). See [Stable and preview versions](/fluid/schema/versions#stable-and-preview-versions).
:::
-0.7.6 adds declarative ownership of infrastructure containers (`packaging`), a declared list of downstream consumers, pins for upstreams in another mesh, and binding-level fields that let one cloud-neutral contract carry encryption, identity mapping and data expiry to each platform it is deployed on.
+0.7.6 adds declarative ownership of infrastructure containers (`packaging`), a declared list of downstream consumers, pins for upstreams in another mesh, binding-level fields that let one cloud-neutral contract carry encryption, identity mapping and data expiry to each platform it is deployed on, row filters and masked columns in an expose's access policy, Apache Flink as a build engine, and credentials by environment variable for a REST catalog an Iceberg sink writes to.
- **Schema:** [`fluid-schema-0.7.6.json`](/fluid/schema/fluid-schema-0.7.6.json) · **Reference:** [`specs/0.7.6/fluid-spec.html`](/fluid/specs/0.7.6/fluid-spec.html) · **Diff:** [`diff-0.7.5-to-0.7.6.md`](https://github.com/open-data-protocol/fluid/blob/main/schema-diffs/diff-0.7.5-to-0.7.6.md)
- **Compatibility:** additive over 0.7.5. `scripts/check-compat.py --from 0.7.5 --to 0.7.6` reports no narrowing change, so a valid 0.7.5 contract stays valid when it declares `"0.7.6"`.
@@ -27,8 +27,12 @@ Every addition below is optional. The list is derived from the schemas: these ar
| `exposes[].binding.governance.lakeFormation.bucketPolicy` | Which grantees may read a Lake Formation location directly from object storage. |
| `exposes[].lifecycle.expire` | Whether data older than the expose's `retention` is deleted. |
| `exposes[].policy.privacy.masking[].params.{keepFirst, keepLast, saltEnv, keyEnv}` | Typed parameters for masking rules. |
+| `exposes[].policy.authz.rowFilters[]` | Which rows each principal may read. |
+| `exposes[].policy.authz.columnRestrictions[].mask` | The masking rule an `access: mask` restriction applies. |
+| `exposes[].binding.governance.lakeFormation.maskedViews` | The role and Glue connection behind the views that serve masked columns on Lake Formation. |
+| `build(s).properties.sink.catalogAuth` | How an acquisition sink authenticates to an Iceberg REST catalog, by naming environment variables. |
-`fluidVersion` accepts `"0.7.6"` in addition to `"0.7.3"`, `"0.7.4"` and `"0.7.5"`.
+`fluidVersion` accepts `"0.7.6"` in addition to `"0.7.3"`, `"0.7.4"` and `"0.7.5"`. Three enums gain values: `build(s).engine` accepts `flink`, `build(s).properties.sink.catalog` accepts `lakekeeper` and `polaris`, and `exposes[].policy.authz.columnRestrictions[].access` accepts `mask`.
0.7.6 also writes down the meaning of a field that already existed: `exposes[].policy.authz.columnRestrictions` ([below](#column-restrictions-semantics-written-down)).
@@ -165,6 +169,110 @@ policy:
`params` stays open (`additionalProperties: true`). The typed keys name secrets by environment variable rather than carrying them; the schema's description adds that the reference implementation's runner refuses any other key, including a literal salt or key. The 0.7.6 description of `strategy` also gives each strategy's output form — for example, `hash` is the lowercase hex SHA-256 of salt followed by value — and notes that `k_anonymity` is accepted by the schema but is a property of a whole table, not a per-value transform.
+## Row filters and masked columns
+
+
+```yaml
+policy:
+ authz:
+ readers: ["group:support", "group:analysts-eu"]
+ columnRestrictions:
+ - principal: "group:support"
+ columns: [phone]
+ access: mask
+ mask: last_four # last_four | first_four | nullify
+ rowFilters:
+ - principal: "group:analysts-eu"
+ name: eu_only # optional; a lowercase identifier
+ where: "region = 'EU'"
+```
+
+**`rowFilters[]`** lets a principal read only the rows its `where` predicate selects. A reader that no filter names reads every row, and the schema's description allows one filter per principal per expose. `where` is one SQL boolean expression over the expose's columns, with no statement separator or comment. `principal` and `where` are required, and `name`, the filter's name on the platform, must match `^[a-z][a-z0-9_]{0,62}$`. Principals resolve through `binding.principals`, as for column restrictions.
+
+**`access: mask`** lets the principal read the restricted columns masked, never their raw values, by the rule in **`mask`**: `last_four` (`XXXXX` and the value's last four characters), `first_four` (its first four characters and `XXXXX`) or `nullify` (`NULL`). The schema requires `mask` with `access: mask` and refuses it with any other `access`. A deny beats a mask, and a mask beats an allow for the principal it names.
+
+
+```yaml
+binding:
+ platform: aws
+ format: parquet
+ location: { bucket: acme-lake, path: support/contacts/, region: eu-west-1, database: support, table: contacts }
+ governance:
+ lakeFormation:
+ maskedViews:
+ definer: "arn:aws:iam::123456789012:role/fluid-view-definer"
+ validationConnection: athena-view-validation
+```
+
+Lake Formation cannot mask a column. **`governance.lakeFormation.maskedViews`** says where the views that serve masked columns come from: `definer`, the IAM role the views are defined as, and `validationConnection`, the Glue connection Glue validates each view's SQL through. Both are required. In the reference implementation, a masked principal's grant on the table excludes the columns, and the principal reads a protected `SECURITY DEFINER` Glue Data Catalog view, `
_masked`, that returns them masked by the same rule and applies the principal's row filter. On BigQuery it attaches a data policy to the columns' policy tag. Like the other descriptions of what `fluid apply` emits, this is non-normative.
+
+## `engine: flink`
+
+
+```yaml
+build:
+ pattern: embedded-logic
+ engine: flink
+ properties:
+ language: flink_sql
+ sql: |
+ INSERT INTO payments_reconciled
+ SELECT p.payment_id, p.amount, s.settled_at
+ FROM payments AS p JOIN settlements AS s ON p.payment_id = s.payment_id
+ parameters:
+ parallelism: 4
+ upgradeMode: savepoint # the operator's savepoint | last-state | stateless
+ flinkConfiguration:
+ execution.checkpointing.interval: 60s
+```
+
+`flink` names Apache Flink as the build's engine, usually for a long-running Flink SQL job (`properties.language: flink_sql`). The embedded-logic properties are a closed set, so deployment settings go under `properties.parameters`; the schema's description recommends the names the Flink Kubernetes Operator's job spec uses (`parallelism`, `upgradeMode`) and `flinkConfiguration`. A 0.7.5 contract cannot declare `engine: flink`.
+
+The reference implementation validates and plans a `flink` build but ships no runner for it: the platform that runs the job, such as the Flink Kubernetes Operator or AWS Managed Service for Apache Flink, executes it. It never runs such a build's SQL on DuckDB or turns it into a warehouse statement, and it treats a build with `properties.language: flink_sql` the same way on any `fluidVersion`.
+
+## REST catalog credentials: `sink.catalogAuth`
+
+
+```yaml
+exposes:
+ - exposeId: events
+ kind: table
+ contract:
+ schema:
+ - { name: event_id, type: STRING, required: true }
+ binding:
+ platform: local
+ format: iceberg
+ location: { catalog: lakekeeper, uri: "http://lakekeeper:8181/catalog", warehouse: analytics, database: default, table: events }
+builds:
+ - id: ingest
+ pattern: acquisition
+ engine: kafka-connect
+ outputs: [events]
+ properties:
+ source: { kind: postgres, mode: incremental_append }
+ sink:
+ format: iceberg
+ catalog: lakekeeper
+ catalogAuth:
+ type: oauth2
+ credentialEnv: LAKEKEEPER_CLIENT # a variable holding client_id:client_secret
+ oauth2ServerUri: https://keycloak.example.com/realms/lake/protocol/openid-connect/token
+ scope: lakekeeper
+```
+
+`catalogAuth` says how the sink authenticates to an Iceberg REST catalog. It names environment variables and never holds a secret.
+
+| `type` | Requires | Refuses |
+|---|---|---|
+| `oauth2` (client credentials) | `credentialEnv`, `oauth2ServerUri`, `scope`; `audience` and `resource` are optional | `tokenEnv` |
+| `bearer` (a static token) | `tokenEnv` | `credentialEnv`, `oauth2ServerUri`, `scope`, `audience`, `resource` |
+
+- `credentialEnv` and `tokenEnv` must be environment variable names. The schema refuses one shaped like a token: a run of 20 or more letters and digits, with a digit, between underscores.
+- `oauth2ServerUri` must be an `http://` or `https://` URL with no `user:password@` part. It, `scope`, `audience` and `resource` may not hold a `${...}` expression or a control character, or start with `env:`.
+
+The reference implementation writes each named variable into the sink config it derives as `env:`, which Apache Iceberg's REST client resolves from the environment of the process that runs the sink. It refuses `catalogAuth` on a catalog it does not reach over REST, and next to a hand-written override of the same settings. It also refuses an Iceberg sink unless an expose the build writes is bound with `format: iceberg`, as `events` is above; the catalog's address is on that binding.
+
## Column restrictions: semantics written down
`exposes[].policy.authz.columnRestrictions` exists in 0.7.5 with the one-line description "Column-level access control". The 0.7.6 description defines it:
@@ -172,13 +280,14 @@ policy:
- A column named in any restriction is restricted.
- `access: deny` — the principal may not read the columns. `access: allow` — the columns are readable only by the principals an `allow` names.
- A deny beats an allow, and a restriction never grants access: the readers are still the expose's readers.
+- `access: mask` (added in 0.7.6) — the principal reads the columns masked by the rule in `mask` ([above](#row-filters-and-masked-columns)). A deny beats a mask, and a mask beats an allow for the principal it names.
- Principals are logical, resolved per platform through `binding.principals`.
---
## Two complete contracts
-Both validate against the 0.7.6 JSON Schema (Draft 2020-12) and with the reference implementation, `data-product-forge` 0.18.1 (`fluid validate` prints `✅ Valid FLUID contract (schema v0.7.6)`).
+Both validate against the 0.7.6 JSON Schema (Draft 2020-12) and with the reference implementation, `data-product-forge` 0.21.0 (`fluid validate` prints `✅ Valid FLUID contract (schema v0.7.6)`).
**BigQuery:** column restriction, mapped principals, a product-owned key, expiring partitions, a percentile measure, packaging and a consumer.
diff --git a/docs/releases/README.md b/docs/releases/README.md
index 0908f15..beb937c 100644
--- a/docs/releases/README.md
+++ b/docs/releases/README.md
@@ -6,7 +6,7 @@ Release notes for the FLUID specification, newest first. Each entry links to its
| Version | Status | Codename | Compatibility | Summary |
|---|---|---|---|---|
-| **[0.7.6](/fluid/releases/0.7.6)** | **Preview** | Declarative Packaging Modes | ✅ Additive over 0.7.5 | Adds top-level `packaging` and `consumers`, cross-mesh pins on `consumes[]` (`upstreamWorkspace` + `upstreamDigest`), `aggParams` on semantic measures, and on bindings `encryption.kms`, `principals` and Lake Formation `bucketPolicy`; an expose's `lifecycle` gains `expire`, and masking rules gain typed `params`. |
+| **[0.7.6](/fluid/releases/0.7.6)** | **Preview** | Declarative Packaging Modes | ✅ Additive over 0.7.5 | Adds top-level `packaging` and `consumers`, cross-mesh pins on `consumes[]` (`upstreamWorkspace` + `upstreamDigest`), `aggParams` on semantic measures, and on bindings `encryption.kms`, `principals` and Lake Formation `bucketPolicy`; an expose's `lifecycle` gains `expire`, masking rules gain typed `params`, and access policies gain `rowFilters` and `access: mask`. Builds can declare `engine: flink`, and an Iceberg sink can name a REST catalog's credentials by environment variable (`sink.catalogAuth`). |
| **[0.7.5](/fluid/releases/0.7.5)** | **Stable (latest)** | Streaming Kafka → Iceberg Sink & Confluent Tableflow | ✅ Additive over 0.7.4 | Adds an opt-in Iceberg streaming sink on the `kafka-connect` acquisition engine, the `confluent` (Tableflow) and `pgvector` binding platforms, the `pgvector_table` format and `binding.vectorConfig`, and location fields for Tableflow, Iceberg catalogs, Redshift Serverless and Kinesis. |
| **[0.7.4](/fluid/releases/0.7.4)** | Stable | Runtime agentPolicy Enforcement at the MCP Gateway | ✅ Additive over 0.7.3 | Adds the optional `exposes[].mcp` block, the `postgres` binding platform, and the `postgres_table` / `athena_table` / `glue_table` formats. |
| **[0.7.3](/fluid/releases/0.7.3)** | Stable | Source-Aligned Acquisition | ✅ Additive over 0.7.2 | The `acquisition` build pattern and six ingestion engines, top-level `retention`, `governance` (AWS Lake Formation) and `extensions`, `binding.governance`, `metadata.productType`, and `exposes[].contract.schemaPolicy`. |
diff --git a/docs/schema/changelog.md b/docs/schema/changelog.md
index 75ee722..cf3cb98 100644
--- a/docs/schema/changelog.md
+++ b/docs/schema/changelog.md
@@ -21,6 +21,11 @@ No narrowing change. **Preview:** the 0.7.6 schema can still change before it is
- `exposes[].binding.governance.lakeFormation.bucketPolicy` (`cross-account` | `none` | `all-grantees`; `cross-account` is the default). Its description says that an emitted `aws_s3_bucket_policy` is authoritative and replaces every other statement on the bucket; see the [0.7.6 notes](/fluid/releases/0.7.6#binding-fields-encryption-principals-lake-formation-bucket-policy).
- `exposes[].lifecycle` now refers to a new `exposeLifecycle` definition: the root `lifecycle` fields plus `expire` (default `false`).
- Typed `exposes[].policy.privacy.masking[].params`: `keepFirst`, `keepLast`, `saltEnv`, `keyEnv`.
+- `exposes[].policy.authz.rowFilters[]` (`principal` and `where` required; `name` a lowercase identifier; `description`).
+- `exposes[].policy.authz.columnRestrictions[].access` value `mask`, with `columnRestrictions[].mask` (`last_four` | `first_four` | `nullify`), required with `access: mask` and refused without it.
+- `exposes[].binding.governance.lakeFormation.maskedViews` (`definer` and `validationConnection`, both required).
+- `build(s).engine` value `flink`; see the [0.7.6 notes](/fluid/releases/0.7.6#engine-flink).
+- `build(s).properties.sink.catalog` values `lakekeeper` and `polaris`, and `build(s).properties.sink.catalogAuth` (`type`: `oauth2` | `bearer`), which names the environment variables holding a REST catalog's credentials.
- `fluidVersion` accepts `"0.7.6"`.
**Described:** `exposes[].policy.authz.columnRestrictions` gains a description of its semantics (deny beats allow; a restriction never grants access; principals are logical).
@@ -147,3 +152,4 @@ No narrowing change. Note that 0.1.0 and 0.1.1 declare the same `$id` (see [Vers
- **Lake Formation `admins` (0.7.3, 0.7.4, 0.7.5).** These schemas were first published with a description of `governance.lakeFormation.admins` saying that principals not listed are not removed. That is the opposite of what applying it does: the list is **authoritative**, and an admin not listed — including the identity running the apply — loses Lake Formation admin. The description was corrected in the reference implementation and the published files carry the corrected text. Only the description changed; the same documents validate.
- **0.7.5** was re-published with the stable content (above).
+- **Lake Formation tags (0.7.3, 0.7.4, 0.7.5, 0.7.6).** These schemas were first published with a description of `exposes[].binding.governance.lakeFormation.tags` saying that its keys must reference tag definitions in the contract's top-level `governance.lakeFormation.tagDefinitions`. The schema never required that, and a key the contract does not define names a tag owned outside it, such as a platform's central LF-tag ontology, which must already exist in the account. The descriptions of `tags` and `tagDefinitions` were corrected in the reference implementation, and the published files carry the corrected text. Only the descriptions changed; the same documents validate.
diff --git a/schema-diffs/diff-0.7.2-to-0.7.3.md b/schema-diffs/diff-0.7.2-to-0.7.3.md
index 494aeb7..e7a7d4b 100644
--- a/schema-diffs/diff-0.7.2-to-0.7.3.md
+++ b/schema-diffs/diff-0.7.2-to-0.7.3.md
@@ -579,7 +579,7 @@
}
tags: {
type: "object"
- description: "LF-tag (TBAC) associations applied to this table. Keys must reference tag definitions declared in th..."
+ description: "LF-tag (TBAC) associations applied to this table, as tag key to value. Emitted as aws_lakeformation_..."
additionalProperties: {
type: "string"
}
diff --git a/schema-diffs/diff-0.7.5-to-0.7.6.md b/schema-diffs/diff-0.7.5-to-0.7.6.md
index a89ef4b..1bcfc0e 100644
--- a/schema-diffs/diff-0.7.5-to-0.7.6.md
+++ b/schema-diffs/diff-0.7.5-to-0.7.6.md
@@ -1,9 +1,9 @@
# Schema Changes: 0.7.5 → 0.7.6
-**Total changes:** 30
-- ✅ Added: 22
+**Total changes:** 40
+- ✅ Added: 27
- ❌ Removed: 0
-- 📝 Modified: 8
+- 📝 Modified: 13
> **Note — 0.7.6 is a preview.** 0.7.5 is the latest stable version. A contract
@@ -19,6 +19,57 @@
## ✅ Added Properties
+### `$defs.acquisitionSink.properties.catalogAuth`
+```json
+{
+ type: "object"
+ description: "NEW in v0.7.6: How the Iceberg sink config forge-cli DERIVES (kafka-connect with sink.format iceberg..."
+ additionalProperties: false
+ required: ["type"]
+ properties: {
+ type: {
+ type: "string"
+ enum: ["oauth2", "bearer"]
+ description: "oauth2: OAuth2 client credentials against oauth2ServerUri. bearer: a static bearer token."
+ }
+ credentialEnv: {
+ type: "string"
+ pattern: "^[A-Za-z_][A-Za-z0-9_]*(?![\\s\\S])"
+ not: {
+ pattern: "(?:^|_)(?=[A-Za-z]*[0-9])[A-Za-z0-9]{20,}(?:_|$)"
+ }
+ description: "type oauth2: the NAME of the environment variable holding client_id:client_secret (Iceberg splits it..."
+ }
+ oauth2ServerUri: {
+ type: "string"
+ pattern: "^https?://[^\\s/?#@]+(?:[/?#]\\S*)?$"
+ not: {
+ pattern: "\\$\\{|[\\x00-\\x1f\\x7f]|^\\s*env:"
+ }
+ description: "type oauth2: absolute URL starting with a lowercase http:// or https:// of the identity provider's t..."
+ }
+ scope: {
+ type: "string"
+ minLength: 1
+ not: {
+ pattern: "\\$\\{|[\\x00-\\x1f\\x7f]|^\\s*env:"
+ }
+ description: "type oauth2: the OAuth2 scope to request (Iceberg's default, catalog, is not one most identity provi..."
+ }
+ audience: {
+ type: "string"
+ minLength: 1
+ not: {
+ pattern: "\\$\\{|[\\x00-\\x1f\\x7f]|^\\s*env:"
+ }
+ description: "type oauth2, optional: sent as the audience parameter on the token request."
+ }
+ ... (2 more)
+ }
+ ... (1 more)
+}
+```
+
### `$defs.binding.allOf`
```json
[{"if": {"properties": {"platform": {"const": "gcp"}}, "required": ["platform"]}, "then": {"properties": {"encryption": {"properties": {"kms": {"not": {"pattern": "^(alias/|arn:)"}}}}, "principals": {"additionalProperties": {"anyOf": [{"type": "string", "pattern": "^(user|group|serviceAccount|domain):.+$"}, {"type": "array", "items": {"type": "string", "pattern": "^(user|group|serviceAccount|domain):.+$"}}]}}}}}, {"if": {"properties": {"platform": {"const": "aws"}}, "required": ["platform"]}, "then": {"properties": {"encryption": {"properties": {"kms": {"not": {"pattern": "^projects/"}}}}, "principals": {"additionalProperties": {"anyOf": [{"type": "string", "pattern": "^arn:aws[a-z0-9-]*:iam::"}, {"type": "array", "items": {"type": "string", "pattern": "^arn:aws[a-z0-9-]*:iam::"}}]}}}}}]
@@ -73,6 +124,26 @@
}
```
+### `$defs.bindingGovernance.properties.lakeFormation.properties.maskedViews`
+```json
+{
+ type: "object"
+ additionalProperties: false
+ required: ["definer", "validationConnection"]
+ description: "Where the protected Glue Data Catalog views of the expose's masked columns come from (NEW in v0.7.6)..."
+ properties: {
+ definer: {
+ type: "string"
+ description: "ARN of the IAM role the views are defined as: Glue and Lake Formation must be allowed to assume it (..."
+ }
+ validationConnection: {
+ type: "string"
+ description: "The Glue connection of type VIEW_VALIDATION_ATHENA (WORKGROUP_NAME) Glue validates each view's SQL t..."
+ }
+ }
+}
+```
+
### `$defs.consumeRef.dependentRequired`
```json
{
@@ -197,6 +268,11 @@
}
```
+### `$defs.exposePolicy.properties.authz.properties.columnRestrictions.items.allOf`
+```json
+[{"if": {"properties": {"access": {"const": "mask"}}, "required": ["access"]}, "then": {"required": ["mask"]}}, {"if": {"required": ["mask"]}, "then": {"properties": {"access": {"const": "mask"}}, "required": ["access"]}}]
+```
+
### `$defs.exposePolicy.properties.authz.properties.columnRestrictions.items.properties.access.description`
```json
"deny: the principal may not read the columns. allow: only the principals an allow names may read the..."
@@ -207,11 +283,52 @@
"Columns of this expose's schema."
```
+### `$defs.exposePolicy.properties.authz.properties.columnRestrictions.items.properties.mask`
+```json
+{
+ type: "string"
+ enum: ["last_four", "first_four", "nullify"]
+ description: "The masking rule an access: mask restriction applies, required with it and refused without it. last_..."
+}
+```
+
### `$defs.exposePolicy.properties.authz.properties.columnRestrictions.items.properties.principal.description`
```json
"Logical principal the restriction applies to, written as in accessPolicy (e.g. group:analysts@compan..."
```
+### `$defs.exposePolicy.properties.authz.properties.rowFilters`
+```json
+{
+ type: "array"
+ description: "Row-level access control (NEW in v0.7.6). Each filter lets its principal read only the rows its wher..."
+ items: {
+ type: "object"
+ additionalProperties: false
+ required: ["principal", "where"]
+ properties: {
+ principal: {
+ type: "string"
+ description: "Logical principal the filter applies to, written as in accessPolicy; binding.principals maps it per ..."
+ }
+ name: {
+ type: "string"
+ pattern: "^[a-z][a-z0-9_]{0,62}$"
+ description: "The filter's name on the platform (the row access policy or data cells filter); derived from the pri..."
+ }
+ where: {
+ type: "string"
+ minLength: 1
+ description: "One SQL boolean expression over the expose's columns, e.g. consent_data_analytics = true AND is_supp..."
+ }
+ description: {
+ type: "string"
+ }
+ }
+ }
+}
+```
+
### `$defs.exposePolicy.properties.privacy.properties.masking.description`
```json
"Columns to treat before they land. The DuckDB acquisition runner rewrites each named column inside t..."
@@ -363,6 +480,30 @@
## 📝 Modified Properties
+### `$defs.acquisitionSink.properties.catalog.enum`
+
+**Before:**
+```json
+[...6 items...]
+```
+
+**After:**
+```json
+[...8 items...]
+```
+
+### `$defs.bindingLocation.properties.catalog.description`
+
+**Before:**
+```json
+"NEW in v0.7.5: Iceberg catalog kind/profile selector — maps to iceberg.catalog.type (e.g. glue, rest..."
+```
+
+**After:**
+```json
+"NEW in v0.7.5: The Iceberg catalog that owns this table: glue, rest, lakekeeper, polaris, unity, nes..."
+```
+
### `$defs.bindingLocation.properties.partitionBy.description`
**Before:**
@@ -375,6 +516,30 @@
"NEW in v0.7.5: Iceberg partition columns — a FLUID abstraction over the Iceberg PARTITIONED BY / Par..."
```
+### `$defs.build.properties.engine.anyOf`
+
+**Before:**
+```json
+[{"enum": ["dbt", "sql", "python", "spark", "glue", "custom", "duckdb", "airbyte", "meltano", "dlt", "kafka-connect", "debezium"]}, {"pattern": "^dbt-[a-z0-9]+([_-][a-z0-9]+)*$"}]
+```
+
+**After:**
+```json
+[{"enum": ["dbt", "sql", "python", "spark", "glue", "custom", "duckdb", "airbyte", "meltano", "dlt", "kafka-connect", "debezium", "flink"]}, {"pattern": "^dbt-[a-z0-9]+([_-][a-z0-9]+)*$"}]
+```
+
+### `$defs.build.properties.engine.description`
+
+**Before:**
+```json
+"Build/ingestion engine. Transformation engines: dbt, sql, python, spark, glue, custom. Adapter-quali..."
+```
+
+**After:**
+```json
+"Build/ingestion engine. Transformation engines: dbt, sql, python, spark, glue, custom. Adapter-quali..."
+```
+
### `$defs.expose.properties.lifecycle.$ref`
**Before:**
@@ -399,6 +564,18 @@
"Column-level access control. A column named in any restriction is restricted. 'deny': the principal ..."
```
+### `$defs.exposePolicy.properties.authz.properties.columnRestrictions.items.properties.access.enum`
+
+**Before:**
+```json
+["allow", "deny"]
+```
+
+**After:**
+```json
+["allow", "deny", "mask"]
+```
+
### `$id`
**Before:**
diff --git a/schema/fluid-schema-0.7.3.json b/schema/fluid-schema-0.7.3.json
index 7f26de9..25589ee 100644
--- a/schema/fluid-schema-0.7.3.json
+++ b/schema/fluid-schema-0.7.3.json
@@ -1386,7 +1386,7 @@
},
"tagDefinitions": {
"type": "object",
- "description": "LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one aws_lakeformation_lf_tag per key; values are the tag's allowed-values list. Field idiom borrowed from OpenMetadata's classification-label model — see ODCS roles + OpenMetadata data contracts spec.",
+ "description": "LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one aws_lakeformation_lf_tag per key; values are the tag's allowed-values list. The apply CREATES every tag defined here, so a key defined here must not also be created elsewhere: not by a platform bootstrap, another contract or by hand in the same account. A tag owned outside the contract is not defined here, and a binding's governance.lakeFormation.tags can still associate it. An association with a key defined here waits for the tag, and its value must be one of the tag's values, compared case-insensitively because Lake Formation stores keys and values lower-cased. A key with no values is refused, and so are two keys that would be one resource name, such as a-b and a_b. Field idiom borrowed from OpenMetadata's classification-label model — see ODCS roles + OpenMetadata data contracts spec.",
"additionalProperties": {
"type": "array",
"items": {"type": "string"},
@@ -1470,7 +1470,7 @@
},
"tags": {
"type": "object",
- "description": "LF-tag (TBAC) associations applied to this table. Keys must reference tag definitions declared in the contract's top-level governance.lakeFormation.tagDefinitions. Emitted as aws_lakeformation_resource_lf_tags.",
+ "description": "LF-tag (TBAC) associations applied to this table, as tag key to value. Emitted as aws_lakeformation_resource_lf_tags; a key whose value is empty is not associated. A key defined in the contract's top-level governance.lakeFormation.tagDefinitions is a tag this contract creates: the association waits for it, and the value must be one of the tag's values. A key the contract does not define is a tag owned outside the contract, such as a platform's central LF-tag ontology: it must already exist in the account, the principal that runs the apply needs the Lake Formation ASSOCIATE permission on it, and only Lake Formation checks its key and value, at apply. A key the contract defines must not also be created elsewhere. Refused when the binding names no Glue table (location.database and location.table).",
"additionalProperties": {"type": "string"}
},
"rowFilter": {
diff --git a/schema/fluid-schema-0.7.4.json b/schema/fluid-schema-0.7.4.json
index 613ea0a..8e2a455 100644
--- a/schema/fluid-schema-0.7.4.json
+++ b/schema/fluid-schema-0.7.4.json
@@ -5289,7 +5289,7 @@
},
"tagDefinitions": {
"type": "object",
- "description": "LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one aws_lakeformation_lf_tag per key; values are the tag's allowed-values list. Field idiom borrowed from OpenMetadata's classification-label model — see ODCS roles + OpenMetadata data contracts spec.",
+ "description": "LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one aws_lakeformation_lf_tag per key; values are the tag's allowed-values list. The apply CREATES every tag defined here, so a key defined here must not also be created elsewhere: not by a platform bootstrap, another contract or by hand in the same account. A tag owned outside the contract is not defined here, and a binding's governance.lakeFormation.tags can still associate it. An association with a key defined here waits for the tag, and its value must be one of the tag's values, compared case-insensitively because Lake Formation stores keys and values lower-cased. A key with no values is refused, and so are two keys that would be one resource name, such as a-b and a_b. Field idiom borrowed from OpenMetadata's classification-label model — see ODCS roles + OpenMetadata data contracts spec.",
"additionalProperties": {
"type": "array",
"items": {
@@ -5384,7 +5384,7 @@
},
"tags": {
"type": "object",
- "description": "LF-tag (TBAC) associations applied to this table. Keys must reference tag definitions declared in the contract's top-level governance.lakeFormation.tagDefinitions. Emitted as aws_lakeformation_resource_lf_tags.",
+ "description": "LF-tag (TBAC) associations applied to this table, as tag key to value. Emitted as aws_lakeformation_resource_lf_tags; a key whose value is empty is not associated. A key defined in the contract's top-level governance.lakeFormation.tagDefinitions is a tag this contract creates: the association waits for it, and the value must be one of the tag's values. A key the contract does not define is a tag owned outside the contract, such as a platform's central LF-tag ontology: it must already exist in the account, the principal that runs the apply needs the Lake Formation ASSOCIATE permission on it, and only Lake Formation checks its key and value, at apply. A key the contract defines must not also be created elsewhere. Refused when the binding names no Glue table (location.database and location.table).",
"additionalProperties": {
"type": "string"
}
diff --git a/schema/fluid-schema-0.7.5.json b/schema/fluid-schema-0.7.5.json
index e21ece2..a0557cc 100644
--- a/schema/fluid-schema-0.7.5.json
+++ b/schema/fluid-schema-0.7.5.json
@@ -5482,7 +5482,7 @@
},
"tagDefinitions": {
"type": "object",
- "description": "LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one aws_lakeformation_lf_tag per key; values are the tag's allowed-values list. Field idiom borrowed from OpenMetadata's classification-label model \u2014 see ODCS roles + OpenMetadata data contracts spec.",
+ "description": "LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one aws_lakeformation_lf_tag per key; values are the tag's allowed-values list. The apply CREATES every tag defined here, so a key defined here must not also be created elsewhere: not by a platform bootstrap, another contract or by hand in the same account. A tag owned outside the contract is not defined here, and a binding's governance.lakeFormation.tags can still associate it. An association with a key defined here waits for the tag, and its value must be one of the tag's values, compared case-insensitively because Lake Formation stores keys and values lower-cased. A key with no values is refused, and so are two keys that would be one resource name, such as a-b and a_b. Field idiom borrowed from OpenMetadata's classification-label model \u2014 see ODCS roles + OpenMetadata data contracts spec.",
"additionalProperties": {
"type": "array",
"items": {
@@ -5577,7 +5577,7 @@
},
"tags": {
"type": "object",
- "description": "LF-tag (TBAC) associations applied to this table. Keys must reference tag definitions declared in the contract's top-level governance.lakeFormation.tagDefinitions. Emitted as aws_lakeformation_resource_lf_tags.",
+ "description": "LF-tag (TBAC) associations applied to this table, as tag key to value. Emitted as aws_lakeformation_resource_lf_tags; a key whose value is empty is not associated. A key defined in the contract's top-level governance.lakeFormation.tagDefinitions is a tag this contract creates: the association waits for it, and the value must be one of the tag's values. A key the contract does not define is a tag owned outside the contract, such as a platform's central LF-tag ontology: it must already exist in the account, the principal that runs the apply needs the Lake Formation ASSOCIATE permission on it, and only Lake Formation checks its key and value, at apply. A key the contract defines must not also be created elsewhere. Refused when the binding names no Glue table (location.database and location.table).",
"additionalProperties": {
"type": "string"
}
diff --git a/schema/fluid-schema-0.7.6.json b/schema/fluid-schema-0.7.6.json
index 28ca63e..3168d07 100644
--- a/schema/fluid-schema-0.7.6.json
+++ b/schema/fluid-schema-0.7.6.json
@@ -1184,7 +1184,7 @@
},
"columnRestrictions": {
"type": "array",
- "description": "Column-level access control. A column named in any restriction is restricted. 'deny': the principal may not read the columns. 'allow': the columns are readable only by the principals an allow names. A deny beats an allow, and a restriction never grants access: the readers are the expose's readers. Principals are logical and resolve through binding.principals. NEW in v0.7.6 (the field itself is older): `fluid apply` enforces it. GCP BigQuery: a Data Catalog taxonomy per product and dataset with fine-grained access control, a policy tag per set of restricted columns that share their readers, attached through the table schema's policyTags, and roles/datacatalog.categoryFineGrainedReader for exactly the expose's readers allowed to read them: the accessPolicy read grantees and the expose's own policy.authz.readers (a denied principal gets an access error on the columns). A restriction on an expose with no reader on the binding is refused, as it would lock the columns for everyone. A restriction's tags and labels are written into the policy tag's description; AWS has no place for them. AWS: each governance.lakeFormation grant with SELECT excludes the restricted columns its principal may not read (table_with_columns.excluded_column_names); a grant's hand-written excludedColumns must agree, and a binding with no Lake Formation grants is refused. `fluid verify` checks the policy tags and their readers on GCP, and the principals' Lake Formation permissions on AWS.",
+ "description": "Column-level access control. A column named in any restriction is restricted. 'deny': the principal may not read the columns. 'allow': the columns are readable only by the principals an allow names. A deny beats an allow, and a restriction never grants access: the readers are the expose's readers. Principals are logical and resolve through binding.principals. NEW in v0.7.6 (the field itself is older): `fluid apply` enforces it. GCP BigQuery: a Data Catalog taxonomy per product and dataset with fine-grained access control, a policy tag per set of restricted columns that share their readers, attached through the table schema's policyTags, and roles/datacatalog.categoryFineGrainedReader for exactly the expose's readers allowed to read them: the accessPolicy read grantees and the expose's own policy.authz.readers (a denied principal gets an access error on the columns). A restriction on an expose with no reader on the binding is refused, as it would lock the columns for everyone. A restriction's tags and labels are written into the policy tag's description; AWS has no place for them. AWS: each governance.lakeFormation grant with SELECT excludes the restricted columns its principal may not read (table_with_columns.excluded_column_names); a grant's hand-written excludedColumns must agree, and a binding with no Lake Formation grants is refused. `fluid verify` checks the policy tags and their readers on GCP, and the principals' Lake Formation permissions on AWS. NEW in v0.7.6: access: mask lets the principal read the columns masked by the platform (see mask): a deny beats a mask, and a mask beats an allow for the principal it names.",
"items": {
"type": "object",
"additionalProperties": false,
@@ -1204,9 +1204,10 @@
"type": "string",
"enum": [
"allow",
- "deny"
+ "deny",
+ "mask"
],
- "description": "deny: the principal may not read the columns. allow: only the principals an allow names may read them."
+ "description": "deny: the principal may not read the columns. allow: only the principals an allow names may read them. mask: the principal reads the columns masked by the platform, by the rule in mask, and never their raw values (NEW in v0.7.6)."
},
"tags": {
"$ref": "#/$defs/tags",
@@ -1215,8 +1216,53 @@
"labels": {
"$ref": "#/$defs/labels",
"description": "Access control rule labels."
+ },
+ "mask": {
+ "type": "string",
+ "enum": [
+ "last_four",
+ "first_four",
+ "nullify"
+ ],
+ "description": "The masking rule an access: mask restriction applies, required with it and refused without it. last_four: 'XXXXX' and the value's last four characters; first_four: its first four characters and 'XXXXX' (a value of four characters or fewer is hashed instead, by each platform); nullify: NULL. GCP BigQuery: a data policy (DATA_MASKING_POLICY, LAST_FOUR_CHARACTERS, FIRST_FOUR_CHARACTERS or ALWAYS_NULL) on the columns' policy tag, and roles/bigquerydatapolicy.maskedReader for the masked principals, who read the same column masked. AWS Glue: Lake Formation cannot mask, so the masked principals' grant on the table excludes the columns and they read a protected Glue Data Catalog view (SECURITY DEFINER, binding.governance.lakeFormation.maskedViews) that returns them masked by the same rule (NEW in v0.7.6)."
}
- }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "access": {
+ "const": "mask"
+ }
+ },
+ "required": [
+ "access"
+ ]
+ },
+ "then": {
+ "required": [
+ "mask"
+ ]
+ }
+ },
+ {
+ "if": {
+ "required": [
+ "mask"
+ ]
+ },
+ "then": {
+ "properties": {
+ "access": {
+ "const": "mask"
+ }
+ },
+ "required": [
+ "access"
+ ]
+ }
+ }
+ ]
}
},
"tags": {
@@ -1226,6 +1272,37 @@
"labels": {
"$ref": "#/$defs/labels",
"description": "Authorization policy labels."
+ },
+ "rowFilters": {
+ "type": "array",
+ "description": "Row-level access control (NEW in v0.7.6). Each filter lets its principal read only the rows its where predicate selects; a reader no filter names reads every row, and a principal may have one filter per expose. The predicate is one SQL boolean expression over this expose's columns, with no statement separator or comment. Principals are logical and resolve through binding.principals. GCP BigQuery: a row access policy (filter_predicate, grantees) per filter, plus one that selects every row for every other reader and writer, since a principal no row access policy names reads no row at all. AWS Glue: a Lake Formation data cells filter (row_filter and the principal's excluded columns), and the principal's SELECT is granted on the filter instead of the table.",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "principal",
+ "where"
+ ],
+ "properties": {
+ "principal": {
+ "type": "string",
+ "description": "Logical principal the filter applies to, written as in accessPolicy; binding.principals maps it per cloud."
+ },
+ "name": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{0,62}$",
+ "description": "The filter's name on the platform (the row access policy or data cells filter); derived from the principal when omitted."
+ },
+ "where": {
+ "type": "string",
+ "minLength": 1,
+ "description": "One SQL boolean expression over the expose's columns, e.g. consent_data_analytics = true AND is_suppressed = false."
+ },
+ "description": {
+ "type": "string"
+ }
+ }
+ }
}
}
},
@@ -1866,7 +1943,7 @@
},
"catalog": {
"type": "string",
- "description": "NEW in v0.7.5: Iceberg catalog kind/profile selector — maps to iceberg.catalog.type (e.g. glue, rest, nessie, hive). REST-fronted catalogs (polaris / unity / snowflake open catalog) map to 'rest'. Consumed by providers/_iceberg_catalog.py and the streaming-sink validator."
+ "description": "NEW in v0.7.5: The Iceberg catalog that owns this table: glue, rest, lakekeeper, polaris, unity, nessie, bigquery, hive, jdbc, hadoop, dynamodb or snowflake-managed. Spellings fold case and '-'/'_', so iceberg_rest == rest and snowflake == snowflake-managed; fluid validate refuses any other value on an Iceberg expose. When absent: glue on platform aws, snowflake-managed on snowflake, rest elsewhere. The REST catalogs (rest, lakekeeper, polaris, unity) need location.uri plus location.warehouse as the catalog's warehouse NAME, not an object-store URI; Lakekeeper serves its Iceberg REST API under /catalog (uri: http://lakekeeper:8181/catalog). On platform aws, a kind other than glue gets no Glue table and no Glue grant: that catalog creates and governs the table. Every emitter (streaming sinks, dbt catalogs.yml, the IaC, policy compile) reads one table, providers/_iceberg_catalog.py."
},
"warehouse": {
"type": "string",
@@ -2801,7 +2878,8 @@
"meltano",
"dlt",
"kafka-connect",
- "debezium"
+ "debezium",
+ "flink"
]
},
{
@@ -2809,7 +2887,7 @@
}
],
"default": "dbt",
- "description": "Build/ingestion engine. Transformation engines: dbt, sql, python, spark, glue, custom. Adapter-qualified dbt engines (dbt-, e.g. dbt-glue, dbt-snowflake, dbt-bigquery) are accepted too and route through the dbt build runner, which derives the adapter from the suffix. Ingestion engines (acquisition pattern only): duckdb (zero-infra files/JDBC), airbyte (350+ SaaS), meltano (Singer 600+ taps), dlt (Python-native), kafka-connect (streaming), debezium (CDC). AWS Glue: glue (Spark ETL jobs managed by Glue)."
+ "description": "Build/ingestion engine. Transformation engines: dbt, sql, python, spark, glue, custom. Adapter-qualified dbt engines (dbt-, e.g. dbt-glue, dbt-snowflake, dbt-bigquery) are accepted too and route through the dbt build runner, which derives the adapter from the suffix. Ingestion engines (acquisition pattern only): duckdb (zero-infra files/JDBC), airbyte (350+ SaaS), meltano (Singer 600+ taps), dlt (Python-native), kafka-connect (streaming), debezium (CDC). AWS Glue: glue (Spark ETL jobs managed by Glue). NEW in v0.7.6: flink (Apache Flink jobs, usually long-running streaming SQL with properties.language: flink_sql). forge-cli validates and plans a flink build but ships no runner for it: the platform that runs the job, such as the Flink Kubernetes Operator or AWS Managed Service for Apache Flink, executes it. Put deployment settings under properties.parameters, for example parallelism and upgradeMode, as the Flink Kubernetes Operator's job spec names them, and flinkConfiguration."
},
"capabilities": {
"type": "array",
@@ -3514,7 +3592,9 @@
"nessie",
"unity",
"snowflake-managed",
- "hive"
+ "hive",
+ "lakekeeper",
+ "polaris"
]
},
"partitionBy": {
@@ -3522,6 +3602,158 @@
"items": {
"type": "string"
}
+ },
+ "catalogAuth": {
+ "type": "object",
+ "description": "NEW in v0.7.6: How the Iceberg sink config forge-cli DERIVES (kafka-connect with sink.format iceberg, embedded debezium) authenticates to an Iceberg REST catalog (rest, lakekeeper, polaris, unity, snowflake-managed). Names an environment variable, never a secret: forge-cli emits the secret as the catalog property value env:, which Apache Iceberg's REST client resolves from the environment of the process that runs the sink (the Kafka Connect worker, or the Debezium Server forge-cli launches). Iceberg drops a property whose variable is unset, so the embedded Debezium run refuses to start without it; forge-cli cannot see a Connect worker's environment. type oauth2 (client credentials): credentialEnv names a variable holding client_id:client_secret, oauth2ServerUri is the identity provider's token endpoint, scope is required (lakekeeper on a Keycloak-fronted Lakekeeper; PRINCIPAL_ROLE:ALL on Polaris with internal auth, whose oauth2ServerUri is /api/catalog/v1/oauth/tokens). type bearer: tokenEnv names a variable holding a bearer token (Unity Catalog OSS). oauth2ServerUri, scope, audience and resource are written as they are and may not hold a ${...} expression or start with env: (Debezium Server would expand it, Kafka Connect would not). fluid validate refuses it on a hand-written sink config, on a catalog not reached over REST, and next to an override that sets rest.auth.type, credential, token, oauth2-server-uri, scope, audience, resource, rest.sigv4-enabled or a header.Authorization.",
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "oauth2",
+ "bearer"
+ ],
+ "description": "oauth2: OAuth2 client credentials against oauth2ServerUri. bearer: a static bearer token."
+ },
+ "credentialEnv": {
+ "type": "string",
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]*(?![\\s\\S])",
+ "not": {
+ "pattern": "(?:^|_)(?=[A-Za-z]*[0-9])[A-Za-z0-9]{20,}(?:_|$)"
+ },
+ "description": "type oauth2: the NAME of the environment variable holding client_id:client_secret (Iceberg splits it on the first ':'). Emitted as credential=env:. A value shaped like a token (a run of 20 or more letters and digits, with a digit, between underscores) is refused."
+ },
+ "oauth2ServerUri": {
+ "type": "string",
+ "pattern": "^https?://[^\\s/?#@]+(?:[/?#]\\S*)?$",
+ "not": {
+ "pattern": "\\$\\{|[\\x00-\\x1f\\x7f]|^\\s*env:"
+ },
+ "description": "type oauth2: absolute URL starting with a lowercase http:// or https:// of the identity provider's token endpoint, with no user:password@ part (the client id and secret go in credentialEnv's variable), e.g. a Keycloak realm's /realms//protocol/openid-connect/token. Emitted as oauth2-server-uri; without it Iceberg would send the credential to the catalog's own v1/oauth/tokens."
+ },
+ "scope": {
+ "type": "string",
+ "minLength": 1,
+ "not": {
+ "pattern": "\\$\\{|[\\x00-\\x1f\\x7f]|^\\s*env:"
+ },
+ "description": "type oauth2: the OAuth2 scope to request (Iceberg's default, catalog, is not one most identity providers know). Emitted as scope."
+ },
+ "audience": {
+ "type": "string",
+ "minLength": 1,
+ "not": {
+ "pattern": "\\$\\{|[\\x00-\\x1f\\x7f]|^\\s*env:"
+ },
+ "description": "type oauth2, optional: sent as the audience parameter on the token request."
+ },
+ "resource": {
+ "type": "string",
+ "minLength": 1,
+ "not": {
+ "pattern": "\\$\\{|[\\x00-\\x1f\\x7f]|^\\s*env:"
+ },
+ "description": "type oauth2, optional: sent as the resource parameter on the token request."
+ },
+ "tokenEnv": {
+ "type": "string",
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]*(?![\\s\\S])",
+ "not": {
+ "pattern": "(?:^|_)(?=[A-Za-z]*[0-9])[A-Za-z0-9]{20,}(?:_|$)"
+ },
+ "description": "type bearer: the NAME of the environment variable holding the bearer token. Emitted as token=env:. A value shaped like a token (a run of 20 or more letters and digits, with a digit, between underscores, such as a dapi... personal access token) is refused."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "oauth2"
+ }
+ },
+ "type": "object"
+ },
+ "then": {
+ "required": [
+ "credentialEnv",
+ "oauth2ServerUri",
+ "scope"
+ ],
+ "allOf": [
+ {
+ "not": {
+ "required": [
+ "tokenEnv"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "if": {
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "bearer"
+ }
+ },
+ "type": "object"
+ },
+ "then": {
+ "required": [
+ "tokenEnv"
+ ],
+ "allOf": [
+ {
+ "not": {
+ "required": [
+ "credentialEnv"
+ ]
+ }
+ },
+ {
+ "not": {
+ "required": [
+ "oauth2ServerUri"
+ ]
+ }
+ },
+ {
+ "not": {
+ "required": [
+ "scope"
+ ]
+ }
+ },
+ {
+ "not": {
+ "required": [
+ "audience"
+ ]
+ }
+ },
+ {
+ "not": {
+ "required": [
+ "resource"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
}
}
},
@@ -5817,7 +6049,7 @@
},
"tagDefinitions": {
"type": "object",
- "description": "LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one aws_lakeformation_lf_tag per key; values are the tag's allowed-values list. Field idiom borrowed from OpenMetadata's classification-label model — see ODCS roles + OpenMetadata data contracts spec.",
+ "description": "LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one aws_lakeformation_lf_tag per key; values are the tag's allowed-values list. The apply CREATES every tag defined here, so a key defined here must not also be created elsewhere: not by a platform bootstrap, another contract or by hand in the same account. A tag owned outside the contract is not defined here, and a binding's governance.lakeFormation.tags can still associate it. An association with a key defined here waits for the tag, and its value must be one of the tag's values, compared case-insensitively because Lake Formation stores keys and values lower-cased. A key with no values is refused, and so are two keys that would be one resource name, such as a-b and a_b. Field idiom borrowed from OpenMetadata's classification-label model — see ODCS roles + OpenMetadata data contracts spec.",
"additionalProperties": {
"type": "array",
"items": {
@@ -5922,7 +6154,7 @@
},
"tags": {
"type": "object",
- "description": "LF-tag (TBAC) associations applied to this table. Keys must reference tag definitions declared in the contract's top-level governance.lakeFormation.tagDefinitions. Emitted as aws_lakeformation_resource_lf_tags.",
+ "description": "LF-tag (TBAC) associations applied to this table, as tag key to value. Emitted as aws_lakeformation_resource_lf_tags; a key whose value is empty is not associated. A key defined in the contract's top-level governance.lakeFormation.tagDefinitions is a tag this contract creates: the association waits for it, and the value must be one of the tag's values. A key the contract does not define is a tag owned outside the contract, such as a platform's central LF-tag ontology: it must already exist in the account, the principal that runs the apply needs the Lake Formation ASSOCIATE permission on it, and only Lake Formation checks its key and value, at apply. A key the contract defines must not also be created elsewhere. Refused when the binding names no Glue table (location.database and location.table).",
"additionalProperties": {
"type": "string"
}
@@ -5966,6 +6198,25 @@
"name",
"rowExpression"
]
+ },
+ "maskedViews": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "definer",
+ "validationConnection"
+ ],
+ "description": "Where the protected Glue Data Catalog views of the expose's masked columns come from (NEW in v0.7.6). Lake Formation cannot mask, so a principal an access: mask column restriction names reads a protected, SECURITY DEFINER multi-dialect view,
_masked, that returns the columns masked by the restriction's rule and applies its row filter; its grant on the table excludes the columns. The definer is granted SELECT with grant option on the table and DESCRIBE on the database; the principal SELECT and DESCRIBE on the view.",
+ "properties": {
+ "definer": {
+ "type": "string",
+ "description": "ARN of the IAM role the views are defined as: Glue and Lake Formation must be allowed to assume it (sts:AssumeRole, sts:SetContext), and the applying principal to pass it (iam:PassRole)."
+ },
+ "validationConnection": {
+ "type": "string",
+ "description": "The Glue connection of type VIEW_VALIDATION_ATHENA (WORKGROUP_NAME) Glue validates each view's SQL through; the applying principal and the definer need glue:PassConnection on it."
+ }
+ }
}
}
}
diff --git a/scripts/schema-versions.json b/scripts/schema-versions.json
index f4bc56c..45e5153 100644
--- a/scripts/schema-versions.json
+++ b/scripts/schema-versions.json
@@ -11,8 +11,8 @@
"repository": "Agenticstiger/forge-cli",
"schemaDir": "fluid_build/schemas",
"previewSource": "fluid_build/schema_manager.py",
- "ref": "v0.18.1",
- "commit": "93e78d4386c70b9e040283f9c5d98a89f901c986"
+ "ref": "v0.21.0",
+ "commit": "91e071e2c2273e2ba06fd0deabfe42d78de82f2e"
},
"latestStable": "0.7.5",
"preview": ["0.7.6"],
diff --git a/specs/0.7.3/fluid-spec.html b/specs/0.7.3/fluid-spec.html
index 4e675f2..0aa14a6 100644
--- a/specs/0.7.3/fluid-spec.html
+++ b/specs/0.7.3/fluid-spec.html
@@ -13316,8 +13316,15 @@
LF-tag (TBAC) associations applied to this table. Keys must reference tag definitions declared in the contract's top-level governance.lakeFormation.tagDefinitions. Emitted as awslakeformationresourcelftags.
-
+
+
LF-tag (TBAC) associations applied to this table, as tag key to value. Emitted as awslakeformationresourcelftags; a key whose value is empty is not associated. A key defined in the contract's top-level governance.lakeFormation.tagDefinitions is a tag this contract creates: the association waits for it, and the value must be one of the tag's values. A key the contract does not define is a tag owned outside the contract, such as a platform's central LF-tag ontology: it must already exist in the account, the principal that runs the apply needs the Lake Formation ASSOCIATE permission on it, and only Lake Formation checks its key and value, at apply. A key the contract defines must not also be created elsewhere. Refused when the binding names no Glue table (location.database and location.table).
LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one awslakeformationlf_tag per key; values are the tag's allowed-values list. Field idiom borrowed from OpenMetadata's classification-label model — see ODCS roles + OpenMetadata data contracts spec.
-
+
+
LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one awslakeformationlftag per key; values are the tag's allowed-values list. The apply CREATES every tag defined here, so a key defined here must not also be created elsewhere: not by a platform bootstrap, another contract or by hand in the same account. A tag owned outside the contract is not defined here, and a binding's governance.lakeFormation.tags can still associate it. An association with a key defined here waits for the tag, and its value must be one of the tag's values, compared case-insensitively because Lake Formation stores keys and values lower-cased. A key with no values is refused, and so are two keys that would be one resource name, such as a-b and ab. Field idiom borrowed from OpenMetadata's classification-label model — see ODCS roles + OpenMetadata data contracts spec.
LF-tag (TBAC) associations applied to this table. Keys must reference tag definitions declared in the contract's top-level governance.lakeFormation.tagDefinitions. Emitted as awslakeformationresourcelftags.
-
+
+
LF-tag (TBAC) associations applied to this table, as tag key to value. Emitted as awslakeformationresourcelftags; a key whose value is empty is not associated. A key defined in the contract's top-level governance.lakeFormation.tagDefinitions is a tag this contract creates: the association waits for it, and the value must be one of the tag's values. A key the contract does not define is a tag owned outside the contract, such as a platform's central LF-tag ontology: it must already exist in the account, the principal that runs the apply needs the Lake Formation ASSOCIATE permission on it, and only Lake Formation checks its key and value, at apply. A key the contract defines must not also be created elsewhere. Refused when the binding names no Glue table (location.database and location.table).
LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one awslakeformationlf_tag per key; values are the tag's allowed-values list. Field idiom borrowed from OpenMetadata's classification-label model — see ODCS roles + OpenMetadata data contracts spec.
-
+
+
LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one awslakeformationlftag per key; values are the tag's allowed-values list. The apply CREATES every tag defined here, so a key defined here must not also be created elsewhere: not by a platform bootstrap, another contract or by hand in the same account. A tag owned outside the contract is not defined here, and a binding's governance.lakeFormation.tags can still associate it. An association with a key defined here waits for the tag, and its value must be one of the tag's values, compared case-insensitively because Lake Formation stores keys and values lower-cased. A key with no values is refused, and so are two keys that would be one resource name, such as a-b and ab. Field idiom borrowed from OpenMetadata's classification-label model — see ODCS roles + OpenMetadata data contracts spec.
LF-tag (TBAC) associations applied to this table. Keys must reference tag definitions declared in the contract's top-level governance.lakeFormation.tagDefinitions. Emitted as awslakeformationresourcelftags.
-
+
+
LF-tag (TBAC) associations applied to this table, as tag key to value. Emitted as awslakeformationresourcelftags; a key whose value is empty is not associated. A key defined in the contract's top-level governance.lakeFormation.tagDefinitions is a tag this contract creates: the association waits for it, and the value must be one of the tag's values. A key the contract does not define is a tag owned outside the contract, such as a platform's central LF-tag ontology: it must already exist in the account, the principal that runs the apply needs the Lake Formation ASSOCIATE permission on it, and only Lake Formation checks its key and value, at apply. A key the contract defines must not also be created elsewhere. Refused when the binding names no Glue table (location.database and location.table).
LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one awslakeformationlf_tag per key; values are the tag's allowed-values list. Field idiom borrowed from OpenMetadata's classification-label model — see ODCS roles + OpenMetadata data contracts spec.
-
+
+
LF-TBAC tag-key definitions. Each entry is a tag key mapped to its allowed string values. Emitted as one awslakeformationlftag per key; values are the tag's allowed-values list. The apply CREATES every tag defined here, so a key defined here must not also be created elsewhere: not by a platform bootstrap, another contract or by hand in the same account. A tag owned outside the contract is not defined here, and a binding's governance.lakeFormation.tags can still associate it. An association with a key defined here waits for the tag, and its value must be one of the tag's values, compared case-insensitively because Lake Formation stores keys and values lower-cased. A key with no values is refused, and so are two keys that would be one resource name, such as a-b and ab. Field idiom borrowed from OpenMetadata's classification-label model — see ODCS roles + OpenMetadata data contracts spec.
Column-level access control. A column named in any restriction is restricted. 'deny': the principal may not read the columns. 'allow': the columns are readable only by the principals an allow names. A deny beats an allow, and a restriction never grants access: the readers are the expose's readers. Principals are logical and resolve through binding.principals. NEW in v0.7.6 (the field itself is older): fluid apply enforces it. GCP BigQuery: a Data Catalog taxonomy per product and dataset with fine-grained access control, a policy tag per set of restricted columns that share their readers, attached through the table schema's policyTags, and roles/datacatalog.categoryFineGrainedReader for exactly the expose's readers allowed to read them: the accessPolicy read grantees and the expose's own policy.authz.readers (a denied principal gets an access error on the columns). A restriction on an expose with no reader on the binding is refused, as it would lock the columns for everyone. A restriction's tags and labels are written into the policy tag's description; AWS has no place for them. AWS: each governance.lakeFormation grant with SELECT excludes the restricted columns its principal may not read (tablewithcolumns.excludedcolumnnames); a grant's hand-written excludedColumns must agree, and a binding with no Lake Formation grants is refused. fluid verify checks the policy tags and their readers on GCP, and the principals' Lake Formation permissions on AWS.
+
Column-level access control. A column named in any restriction is restricted. 'deny': the principal may not read the columns. 'allow': the columns are readable only by the principals an allow names. A deny beats an allow, and a restriction never grants access: the readers are the expose's readers. Principals are logical and resolve through binding.principals. NEW in v0.7.6 (the field itself is older): fluid apply enforces it. GCP BigQuery: a Data Catalog taxonomy per product and dataset with fine-grained access control, a policy tag per set of restricted columns that share their readers, attached through the table schema's policyTags, and roles/datacatalog.categoryFineGrainedReader for exactly the expose's readers allowed to read them: the accessPolicy read grantees and the expose's own policy.authz.readers (a denied principal gets an access error on the columns). A restriction on an expose with no reader on the binding is refused, as it would lock the columns for everyone. A restriction's tags and labels are written into the policy tag's description; AWS has no place for them. AWS: each governance.lakeFormation grant with SELECT excludes the restricted columns its principal may not read (tablewithcolumns.excludedcolumnnames); a grant's hand-written excludedColumns must agree, and a binding with no Lake Formation grants is refused. fluid verify checks the policy tags and their readers on GCP, and the principals' Lake Formation permissions on AWS. NEW in v0.7.6: access: mask lets the principal read the columns masked by the platform (see mask): a deny beats a mask, and a mask beats an allow for the principal it names.
If the conditions in the "If" tab are respected, then the conditions in the "Then" tab should be respected.
+ Otherwise, the conditions in the "Else" tab should be respected.
If the conditions in the "If" tab are respected, then the conditions in the "Then" tab should be respected.
+ Otherwise, the conditions in the "Else" tab should be respected.
Columns to treat before they land. The DuckDB acquisition runner rewrites each named column inside the write, so the file, S3 object or BigQuery load and the DLQ receive the treated value; quality gates judge the source value first. A treated column lands as a string, so declare it with a string type (string, or VARCHAR under a schemaPolicy that compares source type names). A rule the build cannot apply (an unset salt or key, k_anonymity, a column the stream lacks, a non-string declared type, the incremental cursor) refuses the build rather than landing the column untreated. fluid verify checks every non-null value of a masked column for its strategy's shape, on a local file and, through Athena, on an S3+Glue table. The MCP output-port gateway drops masked columns from what it serves.
hash: lowercase hex SHA-256 of salt || value, deterministic so joins work (64 hex characters). mask: every character but the first params.keepFirst and the last params.keepLast replaced by '*', length preserved. tokenize: HMAC-SHA256 token, 32 hex characters, the token the tokenizepii preLand hook makes. encrypt: AES-GCM, written as 'aesgcm:v1:' + base64url(nonce || ciphertext || tag) with the column name as associated data; reversible with the key, not deterministic. kanonymity: accepted by the schema but refused at landing: it is a property of a whole table over its quasi-identifiers, not of one value.
Strategy parameters. The runner accepts exactly these keys and refuses any other, including a literal salt or key: secrets come only from the environment, never the contract, which is published with the plan, the bundle and the Glue table's fluid_contract parameter. Contracts on 0.7.5 and earlier may use the same keys; their schemas leave params untyped, and the runner checks them at build time.
deny: the principal may not read the columns. allow: only the principals an allow names may read them. mask: the principal reads the columns masked by the platform, by the rule in mask, and never their raw values (NEW in v0.7.6).
NEW in v0.7.6: hash only. The environment variable holding the salt, at least 16 bytes. Unset or empty, the build is refused: an unsalted SHA-256 of a phone number is reversed by enumerating the numbering plan.
NEW in v0.7.6: tokenize and encrypt. The environment variable holding the key. tokenize defaults to FLUIDPIITOKENIZATIONKEY and needs at least 32 bytes; encrypt defaults to FLUIDPIIENCRYPTIONSECRET_KEY and needs the base64 of a 16, 24 or 32-byte AES key (openssl rand -base64 32). Unset or empty, the build is refused.
The masking rule an access: mask restriction applies, required with it and refused without it. lastfour: 'XXXXX' and the value's last four characters; firstfour: its first four characters and 'XXXXX' (a value of four characters or fewer is hashed instead, by each platform); nullify: NULL. GCP BigQuery: a data policy (DATAMASKINGPOLICY, LASTFOURCHARACTERS, FIRSTFOURCHARACTERS or ALWAYS_NULL) on the columns' policy tag, and roles/bigquerydatapolicy.maskedReader for the masked principals, who read the same column masked. AWS Glue: Lake Formation cannot mask, so the masked principals' grant on the table excludes the columns and they read a protected Glue Data Catalog view (SECURITY DEFINER, binding.governance.lakeFormation.maskedViews) that returns them masked by the same rule (NEW in v0.7.6).
+
+
+
+
+
+
Must be one of:
+
"last_four"
"first_four"
"nullify"
+
-
Value must be greater or equal to 0 and lesser or equal to 64
NEW in v0.7.6: mask only. Trailing characters left as they are, so +46701234567 lands as ********4567. A value no longer than keepFirst + keepLast is masked entirely.
-
-
-
-
-
Value must be greater or equal to 0 and lesser or equal to 64
Row-level access control (NEW in v0.7.6). Each filter lets its principal read only the rows its where predicate selects; a reader no filter names reads every row, and a principal may have one filter per expose. The predicate is one SQL boolean expression over this expose's columns, with no statement separator or comment. Principals are logical and resolve through binding.principals. GCP BigQuery: a row access policy (filterpredicate, grantees) per filter, plus one that selects every row for every other reader and writer, since a principal no row access policy names reads no row at all. AWS Glue: a Lake Formation data cells filter (rowfilter and the principal's excluded columns), and the principal's SELECT is granted on the filter instead of the table.
Columns to treat before they land. The DuckDB acquisition runner rewrites each named column inside the write, so the file, S3 object or BigQuery load and the DLQ receive the treated value; quality gates judge the source value first. A treated column lands as a string, so declare it with a string type (string, or VARCHAR under a schemaPolicy that compares source type names). A rule the build cannot apply (an unset salt or key, k_anonymity, a column the stream lacks, a non-string declared type, the incremental cursor) refuses the build rather than landing the column untreated. fluid verify checks every non-null value of a masked column for its strategy's shape, on a local file and, through Athena, on an S3+Glue table. The MCP output-port gateway drops masked columns from what it serves.
hash: lowercase hex SHA-256 of salt || value, deterministic so joins work (64 hex characters). mask: every character but the first params.keepFirst and the last params.keepLast replaced by '*', length preserved. tokenize: HMAC-SHA256 token, 32 hex characters, the token the tokenizepii preLand hook makes. encrypt: AES-GCM, written as 'aesgcm:v1:' + base64url(nonce || ciphertext || tag) with the column name as associated data; reversible with the key, not deterministic. kanonymity: accepted by the schema but refused at landing: it is a property of a whole table over its quasi-identifiers, not of one value.
Strategy parameters. The runner accepts exactly these keys and refuses any other, including a literal salt or key: secrets come only from the environment, never the contract, which is published with the plan, the bundle and the Glue table's fluid_contract parameter. Contracts on 0.7.5 and earlier may use the same keys; their schemas leave params untyped, and the runner checks them at build time.
NEW in v0.7.6: hash only. The environment variable holding the salt, at least 16 bytes. Unset or empty, the build is refused: an unsalted SHA-256 of a phone number is reversed by enumerating the numbering plan.
+Must match regular expression: ^[A-Z_][A-Z0-9_]{0,127}$
-
-
NEW in v0.7.6: tokenize and encrypt. The environment variable holding the key. tokenize defaults to FLUIDPIITOKENIZATIONKEY and needs at least 32 bytes; encrypt defaults to FLUIDPIIENCRYPTIONSECRET_KEY and needs the base64 of a 16, 24 or 32-byte AES key (openssl rand -base64 32). Unset or empty, the build is refused.
+Must match regular expression: ^[A-Z_][A-Z0-9_]{0,127}$
-
NEW in v0.7.6: mask only. Trailing characters left as they are, so +46701234567 lands as ********4567. A value no longer than keepFirst + keepLast is masked entirely.
+
-
+
Value must be greater or equal to 0 and lesser or equal to 64
If the conditions in the "If" tab are respected, then the conditions in the "Then" tab should be respected.
- Otherwise, the conditions in the "Else" tab should be respected.
If the conditions in the "If" tab are respected, then the conditions in the "Then" tab should be respected.
Otherwise, the conditions in the "Else" tab should be respected.
Target platform. NEW in v0.7.5: 'pgvector' for the vector/embeddings output port (pgvector-on-PostgreSQL RAG target — emits the embeddings table + ANN index DDL for the ai-embeddable columns). NEW in v0.7.5: 'confluent' for the Confluent Cloud Tableflow managed Kafka→Iceberg emitter. NEW in v0.7.4: 'postgres' for the MCP output-port PostgreSQL driver (AWS Athena uses platform 'aws').
If the conditions in the "If" tab are respected, then the conditions in the "Then" tab should be respected.
+ Otherwise, the conditions in the "Else" tab should be respected.
Binding format. NEW in v0.7.5: 'pgvectortable' — a pgvector embeddings table target for the vector output port. NEW in v0.7.4: 'postgrestable' (PostgreSQL driver), 'athenatable' / 'gluetable' (AWS Athena driver) for the MCP output port.
Target platform. NEW in v0.7.5: 'pgvector' for the vector/embeddings output port (pgvector-on-PostgreSQL RAG target — emits the embeddings table + ANN index DDL for the ai-embeddable columns). NEW in v0.7.5: 'confluent' for the Confluent Cloud Tableflow managed Kafka→Iceberg emitter. NEW in v0.7.4: 'postgres' for the MCP output-port PostgreSQL driver (AWS Athena uses platform 'aws').
Binding format. NEW in v0.7.5: 'pgvectortable' — a pgvector embeddings table target for the vector output port. NEW in v0.7.4: 'postgrestable' (PostgreSQL driver), 'athenatable' / 'gluetable' (AWS Athena driver) for the MCP output port.
NEW in v0.7.5: vector / embeddings output-port configuration (platform 'pgvector', format 'pgvectortable'). Consumed by fluidbuild.output_ports.vector to emit the pgvector embeddings-table + ANN-index DDL and a RAG provenance manifest for the schema's ai-embeddable columns.
Embedding vector width (e.g. 1536 for OpenAI text-embedding-3-small, 1024 for Cohere embed-english-light). Maps to the pgvector column type vector(<dimensions>).
Provenance: the embedding model that produces the vectors (e.g. 'text-embedding-3-small'). Recorded in the RAG manifest and the embeddings-table's embedding_model column so a retriever can verify model parity.
ANN index built on the embedding column. 'hnsw' (higher recall / lower query latency, buildable before data lands), 'ivfflat' (lower build cost, needs data first), or 'none' (no index — exact search only).
Distance function the index optimises for → pgvector operator class: cosine→vectorcosineops, l2→vectorl2ops, innerproduct→vectoripops, l1→vectorl1_ops.
Provenance: the source primary-key column that ties each embedding row back to its origin row (written to the embeddings-table's source_key column). Enables update/delete propagation from the source product to the vector store.
NEW in v0.7.5: vector / embeddings output-port configuration (platform 'pgvector', format 'pgvectortable'). Consumed by fluidbuild.output_ports.vector to emit the pgvector embeddings-table + ANN-index DDL and a RAG provenance manifest for the schema's ai-embeddable columns.
Embedding vector width (e.g. 1536 for OpenAI text-embedding-3-small, 1024 for Cohere embed-english-light). Maps to the pgvector column type vector(<dimensions>).
-
+
Value must be greater or equal to 1 and lesser or equal to 16000
Provenance: the embedding model that produces the vectors (e.g. 'text-embedding-3-small'). Recorded in the RAG manifest and the embeddings-table's embedding_model column so a retriever can verify model parity.
ANN index built on the embedding column. 'hnsw' (higher recall / lower query latency, buildable before data lands), 'ivfflat' (lower build cost, needs data first), or 'none' (no index — exact search only).
Distance function the index optimises for → pgvector operator class: cosine→vectorcosineops, l2→vectorl2ops, innerproduct→vectoripops, l1→vectorl1_ops.
Provenance: the source primary-key column that ties each embedding row back to its origin row (written to the embeddings-table's source_key column). Enables update/delete propagation from the source product to the vector store.
NEW in v0.7.5: ARN of the pre-created AWS IAM role Confluent Tableflow assumes (byobaws). Its trust policy is updated post-apply with the provider integration's externalid (two-phase IAM).
NEW in v0.7.5: Iceberg catalog kind/profile selector — maps to iceberg.catalog.type (e.g. glue, rest, nessie, hive). REST-fronted catalogs (polaris / unity / snowflake open catalog) map to 'rest'. Consumed by providers/icebergcatalog.py and the streaming-sink validator.
NEW in v0.7.5: Iceberg warehouse — the canonical Iceberg REST 'warehouse' property (maps to iceberg.catalog.warehouse): an object-store URI (s3://|gs://|abfss://) for object-store catalogs, or the catalog/warehouse name for a REST catalog.
NEW in v0.7.5: Iceberg partition columns — a FLUID abstraction over the Iceberg PARTITIONED BY / PartitionSpec (maps to iceberg.tables.default-partition-by). On a gcp BigQuery table with exposes[].lifecycle.expire: true (fluid-schema 0.7.6), one DATE, TIMESTAMP or DATETIME column to partition by (time_partitioning.field); without it the table is partitioned by ingestion time. With a column, retention counts from the date in that column, not from landing: BigQuery deletes a partition retention after its date, so a backfill of older rows is deleted at once. Leave it out for the S3 rule's semantics (age since written).
Per-resource governance: registerLocation, principal grants, tag associations, row/column filters. Account-wide governance (admins, tag definitions) lives at the contract top-level governance block.
When true, emit awslakeformationresource that registers the binding's S3 path with Lake Formation. Required before LF can manage access to objects under that path.
NEW in v0.7.6: Which grants[] principals get a statement in the awss3bucketpolicy emitted beside the grants (s3:ListBucket and s3:GetBucketLocation on the bucket, s3:GetObject under location.path). A statement lets its principal read the objects straight from S3, which skips Lake Formation's column, row and cell filters and its revocations. 'cross-account' (the default): only principals whose ARN names an AWS account other than the one running the apply, decided at plan time against the caller identity; same-account grantees get no statement, since on a registered location Lake Formation vends them credentials. 'none': no bucket policy at all; the tightest setting when the location is registered (registerLocation) and every reader, cross-account ones included, queries through a Lake Formation integrated engine, or when the bucket policy is managed elsewhere. 'all-grantees': every grantee, same-account included, which is the output before this field existed and reopens the direct S3 read path. An emitted awss3bucketpolicy is authoritative: it replaces every other statement on the bucket. When no grantee is left none is emitted.
Principal-based LF grants on this exposure's database/table. Each entry maps a principal to a permission set (and optional column projection). Emitted as awslakeformationpermissions resources. Field idiom adapted from ODCS v3 roles[] (principal + access-type), bound to IAM ARNs since LF is cloud-specific.
NEW in v0.7.5: ARN of the pre-created AWS IAM role Confluent Tableflow assumes (byobaws). Its trust policy is updated post-apply with the provider integration's externalid (two-phase IAM).
IAM principal ARN (role or user) receiving the grant.
-Must match regular expression: ^arn:aws[a-z0-9-]*:iam::
+ catalog
Type: string
+
+
NEW in v0.7.5: The Iceberg catalog that owns this table: glue, rest, lakekeeper, polaris, unity, nessie, bigquery, hive, jdbc, hadoop, dynamodb or snowflake-managed. Spellings fold case and '-'/'', so icebergrest == rest and snowflake == snowflake-managed; fluid validate refuses any other value on an Iceberg expose. When absent: glue on platform aws, snowflake-managed on snowflake, rest elsewhere. The REST catalogs (rest, lakekeeper, polaris, unity) need location.uri plus location.warehouse as the catalog's warehouse NAME, not an object-store URI; Lakekeeper serves its Iceberg REST API under <host>/catalog (uri: http://lakekeeper:8181/catalog). On platform aws, a kind other than glue gets no Glue table and no Glue grant: that catalog creates and governs the table. Every emitter (streaming sinks, dbt catalogs.yml, the IaC, policy compile) reads one table, providers/icebergcatalog.py.
NEW in v0.7.5: Iceberg warehouse — the canonical Iceberg REST 'warehouse' property (maps to iceberg.catalog.warehouse): an object-store URI (s3://|gs://|abfss://) for object-store catalogs, or the catalog/warehouse name for a REST catalog.
NEW in v0.7.5: Iceberg partition columns — a FLUID abstraction over the Iceberg PARTITIONED BY / PartitionSpec (maps to iceberg.tables.default-partition-by). On a gcp BigQuery table with exposes[].lifecycle.expire: true (fluid-schema 0.7.6), one DATE, TIMESTAMP or DATETIME column to partition by (time_partitioning.field); without it the table is partitioned by ingestion time. With a column, retention counts from the date in that column, not from landing: BigQuery deletes a partition retention after its date, so a backfill of older rows is deleted at once. Leave it out for the S3 rule's semantics (age since written).
Optional column-level restriction. When set, the grant is emitted against awslakeformationpermissions.tablewithcolumns instead of .table, allowing the principal to read only the listed columns.
Per-resource governance: registerLocation, principal grants, tag associations, row/column filters. Account-wide governance (admins, tag definitions) lives at the contract top-level governance block.
LF-tag (TBAC) associations applied to this table. Keys must reference tag definitions declared in the contract's top-level governance.lakeFormation.tagDefinitions. Emitted as awslakeformationresourcelftags.
When true, emit awslakeformationresource that registers the binding's S3 path with Lake Formation. Required before LF can manage access to objects under that path.
@@ -17249,19 +17469,23 @@
-
+
+
+
+
+
-
+
-
+
-
-
Each additional property must conform to the following schema
NEW in v0.7.6: Which grants[] principals get a statement in the awss3bucketpolicy emitted beside the grants (s3:ListBucket and s3:GetBucketLocation on the bucket, s3:GetObject under location.path). A statement lets its principal read the objects straight from S3, which skips Lake Formation's column, row and cell filters and its revocations. 'cross-account' (the default): only principals whose ARN names an AWS account other than the one running the apply, decided at plan time against the caller identity; same-account grantees get no statement, since on a registered location Lake Formation vends them credentials. 'none': no bucket policy at all; the tightest setting when the location is registered (registerLocation) and every reader, cross-account ones included, queries through a Lake Formation integrated engine, or when the bucket policy is managed elsewhere. 'all-grantees': every grantee, same-account included, which is the output before this field existed and reopens the direct S3 read path. An emitted awss3bucketpolicy is authoritative: it replaces every other statement on the bucket. When no grantee is left none is emitted.
Row-level (and optionally column-level) filter applied to all reads of this table via LF. Emitted as awslakeformationdatacellsfilter. The filter must then be granted to principals via grants[].permissions including SELECT — LF enforces both layers.
Principal-based LF grants on this exposure's database/table. Each entry maps a principal to a permission set (and optional column projection). Emitted as awslakeformationpermissions resources. Field idiom adapted from ODCS v3 roles[] (principal + access-type), bound to IAM ARNs since LF is cloud-specific.
Optional column-level restriction. When set, the grant is emitted against awslakeformationpermissions.tablewithcolumns instead of .table, allowing the principal to read only the listed columns.
NEW in v0.7.6: Per-exposure packaging override — key-wise precedence over the contract-wide top-level packaging block (binding.packaging > packaging > absent-legacy).
NEW in v0.7.6: Pool id of the platform-owned tenant pool this product writes into. REQUIRED (enforced by the resolver) whenever any container resolves 'shared' — a pool must be addressable. Propagated as the fluid_pool label/tag for cost attribution.
NEW in v0.7.6: Optional path to the platform team's pool manifest file; snapshotted into the bundle at stage 1 so bundleDigest covers it (any pool-file edit invalidates downstream plans).
LF-tag (TBAC) associations applied to this table, as tag key to value. Emitted as awslakeformationresourcelftags; a key whose value is empty is not associated. A key defined in the contract's top-level governance.lakeFormation.tagDefinitions is a tag this contract creates: the association waits for it, and the value must be one of the tag's values. A key the contract does not define is a tag owned outside the contract, such as a platform's central LF-tag ontology: it must already exist in the account, the principal that runs the apply needs the Lake Formation ASSOCIATE permission on it, and only Lake Formation checks its key and value, at apply. A key the contract defines must not also be created elsewhere. Refused when the binding names no Glue table (location.database and location.table).
+
+
+
+
+
-
+
-
+
-
+
-
-
+
+
Each additional property must conform to the following schema
Row-level (and optionally column-level) filter applied to all reads of this table via LF. Emitted as awslakeformationdatacellsfilter. The filter must then be granted to principals via grants[].permissions including SELECT — LF enforces both layers.
NEW in v0.7.6: Ownership of the Confluent environment/cluster. v1 accepts 'shared' only — dedicated-cluster provisioning is not yet supported (the resolver rejects 'isolated').
NEW in v0.7.6: Encryption at rest for an AWS S3 binding or a GCP BigQuery binding. Absent: nothing is emitted, and the cloud's default applies: SSE-S3 for new S3 objects, Google-managed keys for BigQuery.
NEW in v0.7.6: The KMS key the bucket's objects are encrypted with (SSE-KMS with an S3 Bucket Key). 'product' (the default): fluid apply creates a customer managed key for each bucket this product owns (awskmskey, rotation on, alias alias/fluid/<contract id>/<bucket>) and makes it the bucket's default encryption (awss3bucketserversideencryptionconfiguration). Its key policy lets the account's IAM policies decide who may use it (the KMS default statement for the account root) and, when the binding carries governance.lakeFormation, lets the Lake Formation service-linked role AWSServiceRoleForLakeFormationDataAccess encrypt and decrypt, matched by aws:PrincipalArn so the role need not exist yet. A principal querying through Athena with credentials Lake Formation vends then needs no KMS permission of its own; a grantee in another account that the bucket policy lets read the objects directly (governance.lakeFormation.bucketPolicy) is also allowed kms:Decrypt, through S3 only. The writer (the identity the build runs as) needs kms:GenerateDataKey and kms:Decrypt from IAM. tofu destroy deletes the alias and schedules the key for deletion after 7 days, the minimum KMS allows; kms:CancelKeyDeletion recovers it until then. Dropping the block, or naming another key, removes the product key while the bucket keeps the objects encrypted under it, which become unreadable once the deletion window ends unless they are rewritten or the deletion is cancelled; fluid apply refuses that plan without --allow-data-loss. On a shared (pool) bucket the bucket's default encryption is the pool owner's, so 'product' is refused there. 'alias/<name>' or a key or alias ARN: an existing key, looked up at plan time (data awskmskey, so the identity running fluid apply needs kms:DescribeKey on it) and made the default encryption of a bucket this product owns by its key ARN, since S3 resolves an alias in the account of whoever writes; the plan fails unless the key is Enabled and a symmetric encryption key (SYMMETRICDEFAULT). On a pool bucket it is only checked. Its key policy is its owner's, and must let the Lake Formation role use it when the location is registered: an AWS managed key cannot be used with that role, so alias/aws/s3 is refused with registerLocation, and the plan fails for any key that is not customer managed. 'none': nothing is emitted. fluid verify checks that the key is Enabled and that the objects under the binding's prefix are SSE-KMS with it. On a gcp binding (fluid-schema 0.7.6, BigQuery tables): 'product' creates a Cloud KMS key ring and crypto key for each dataset this product writes (googlekmskeyring fluid-<contract id>-<dataset> and googlekmscryptokey 'bigquery', in the dataset's location, EU and US mapping to the europe and us key locations, rotated every 90 days), grants the project's BigQuery service agent roles/cloudkms.cryptoKeyEncrypterDecrypter on it, and makes it the dataset's defaultencryptionconfiguration and the table's encryptionconfiguration. The project must have the Cloud KMS API (cloudkms.googleapis.com) enabled. A key ring and key cannot be deleted on GCP: tofu destroy removes them from state and schedules the key's versions for destruction (30 days by default), and a later apply adopts the same names. 'projects/<p>/locations/<l>/keyRings/<r>/cryptoKeys/<k>': an existing key in the dataset's location, used as given; its owner grants the BigQuery service agent. 'none': Google-managed keys. An alias/... or arn:... value is refused on a gcp binding, and a Cloud KMS name on an aws binding. Adding a key to a table that exists replaces the table (BigQuery cannot re-key it in place), so fluid apply refuses that plan without --allow-data-loss; the next build lands the data again. fluid verify checks kmsKeyName on the dataset and the table.
Type: object
-Must match regular expression: ^alias/[A-Za-z0-9/_-]{1,250}$
+ maskedViews
Type: object
+
Where the protected Glue Data Catalog views of the expose's masked columns come from (NEW in v0.7.6). Lake Formation cannot mask, so a principal an access: mask column restriction names reads a protected, SECURITY DEFINER multi-dialect view, <table>_masked, that returns the columns masked by the restriction's rule and applies its row filter; its grant on the table excludes the columns. The definer is granted SELECT with grant option on the table and DESCRIBE on the database; the principal SELECT and DESCRIBE on the view.
Type: object
-Must match regular expression: ^arn:aws[a-z-]*:kms:[a-z0-9-]+:[0-9]{12}:(key|alias)/[A-Za-z0-9/_-]{1,250}$
+ definer
Type: string
+
ARN of the IAM role the views are defined as: Glue and Lake Formation must be allowed to assume it (sts:AssumeRole, sts:SetContext), and the applying principal to pass it (iam:PassRole).
The Glue connection of type VIEWVALIDATIONATHENA (WORKGROUP_NAME) Glue validates each view's SQL through; the applying principal and the definer need glue:PassConnection on it.
NEW in v0.7.6: Maps the contract's LOGICAL principals (accessPolicy.grants[].principal and exposes[].policy.authz.columnRestrictions[].principal, keyed exactly as the contract writes them) to the identities they stand for on this binding's platform, so the base contract stays cloud-neutral and each environment's overlay binds it. On gcp an identity is an IAM member (user:, group:, serviceAccount: or domain:); on aws an IAM principal ARN. A value is one identity or a list; [] says the principal has no identity on this cloud, and nothing is granted to it there. When the block is present, every principal the contract names for this expose must be a key: an unmapped one is refused, never emitted as written. Without the block the principals are used as written, except that a GCP principal in a reserved top-level domain (.example, .test, .invalid, .localhost) is refused as a placeholder. Modelled on ODCS v3's roles[] bound per server (servers[].roles) and on dbt grants resolved per target.
NEW in v0.7.6: Per-exposure packaging override — key-wise precedence over the contract-wide top-level packaging block (binding.packaging > packaging > absent-legacy).
+
-
-
-
-
+ No Additional Properties
-
+
-
+
-
+
-
-
Each additional property must conform to the following schema
NEW in v0.7.6: Pool id of the platform-owned tenant pool this product writes into. REQUIRED (enforced by the resolver) whenever any container resolves 'shared' — a pool must be addressable. Propagated as the fluid_pool label/tag for cost attribution.
NEW in v0.7.6: Optional path to the platform team's pool manifest file; snapshotted into the bundle at stage 1 so bundleDigest covers it (any pool-file edit invalidates downstream plans).
NEW in v0.7.6: An expose's lifecycle. The same fields as the contract-root lifecycle, plus expire, which is only meaningful where there is storage to expire.
How long this expose's data is kept (ISO-8601 duration, e.g. P30D, P7Y). A declaration unless expire is true. With expire: true on an AWS S3 binding, fluid apply expires the objects under the binding's prefix this long after they are written; on a GCP BigQuery table it deletes each daily partition this long after its day ends. Years and months are counted as 365 and 30 days, and a part of a day rounds up to a whole one.
NEW in v0.7.6: Delete stored data once it is older than retention. Default false, so a contract that declares a retention period does not start deleting data when forge-cli is upgraded. Honoured by the AWS emitter on an S3 binding and by the GCP emitter on a BigQuery table; on a gcp binding that is not a BigQuery table (GCS, Pub/Sub, Iceberg storage) it is refused rather than dropped. AWS: fluid apply writes one rule per expose into the bucket's awss3bucketlifecycleconfiguration, filtered to the binding's prefix (location.path, else {database}/{table}/; a binding with neither is refused, so no rule ever expires a whole bucket). The rule expires the current version retention after it is written, removes a noncurrent version (on a versioning-enabled bucket) one day after it stops being current, and aborts an incomplete multipart upload after 7 days or the retention period if shorter. The lifecycle configuration is AUTHORITATIVE for the whole bucket: S3 keeps one per bucket, so rules that fluid apply did not write are replaced. It is therefore written only for a bucket this product owns; on a shared (pool) bucket (packaging) nothing is written, the pool's owner must hold the rule, and fluid verify checks that an enabled rule covering the prefix expires objects after exactly this period. Objects already older than the period when the rule is first applied are expired on S3's next lifecycle run, typically within a day. Removing expire later deletes the lifecycle configuration, which fluid apply refuses without --allow-data-loss (it refuses every plan that removes a resource holding data or policy; removing an access grant or a policy tag is a revocation and is not gated); tofu destroy deletes it. GCP (fluid-schema 0.7.6): the BigQuery table is partitioned by day (timepartitioning type DAY, on binding.location.partitionBy when it names one date or timestamp column, else by ingestion time) with expirationms = retention in days, so BigQuery deletes each partition that long after its day ends. BigQuery counts from the partition's date, not from when its rows were written: by ingestion time (no partitionBy, the default and the S3 rule's semantics) no row is deleted sooner than retention after it landed; with partitionBy, retention is the age of the date in that column, so a backfill of rows whose date is already older than retention lands in expired partitions and is deleted at once. It is never a whole-table expiration, which would delete the product. BigQuery cannot partition a table that exists, so adding expire to a live table replaces it: fluid apply refuses that plan without --allow-data-loss, and the next build lands the data again. A later change of retention is an in-place update. Removing expire from a live table removes the partitioning's replacement trigger, which fluid apply also refuses without --allow-data-loss; BigQuery cannot un-partition a table, so to keep data longer set a longer retention instead. fluid verify checks the live table's partition expiration.
NEW in v0.7.6: Ownership of the Confluent environment/cluster. v1 accepts 'shared' only — dedicated-cluster provisioning is not yet supported (the resolver rejects 'isolated').
NEW in v0.7.6: Encryption at rest for an AWS S3 binding or a GCP BigQuery binding. Absent: nothing is emitted, and the cloud's default applies: SSE-S3 for new S3 objects, Google-managed keys for BigQuery.
NEW in v0.7.6: The KMS key the bucket's objects are encrypted with (SSE-KMS with an S3 Bucket Key). 'product' (the default): fluid apply creates a customer managed key for each bucket this product owns (awskmskey, rotation on, alias alias/fluid/<contract id>/<bucket>) and makes it the bucket's default encryption (awss3bucketserversideencryptionconfiguration). Its key policy lets the account's IAM policies decide who may use it (the KMS default statement for the account root) and, when the binding carries governance.lakeFormation, lets the Lake Formation service-linked role AWSServiceRoleForLakeFormationDataAccess encrypt and decrypt, matched by aws:PrincipalArn so the role need not exist yet. A principal querying through Athena with credentials Lake Formation vends then needs no KMS permission of its own; a grantee in another account that the bucket policy lets read the objects directly (governance.lakeFormation.bucketPolicy) is also allowed kms:Decrypt, through S3 only. The writer (the identity the build runs as) needs kms:GenerateDataKey and kms:Decrypt from IAM. tofu destroy deletes the alias and schedules the key for deletion after 7 days, the minimum KMS allows; kms:CancelKeyDeletion recovers it until then. Dropping the block, or naming another key, removes the product key while the bucket keeps the objects encrypted under it, which become unreadable once the deletion window ends unless they are rewritten or the deletion is cancelled; fluid apply refuses that plan without --allow-data-loss. On a shared (pool) bucket the bucket's default encryption is the pool owner's, so 'product' is refused there. 'alias/<name>' or a key or alias ARN: an existing key, looked up at plan time (data awskmskey, so the identity running fluid apply needs kms:DescribeKey on it) and made the default encryption of a bucket this product owns by its key ARN, since S3 resolves an alias in the account of whoever writes; the plan fails unless the key is Enabled and a symmetric encryption key (SYMMETRICDEFAULT). On a pool bucket it is only checked. Its key policy is its owner's, and must let the Lake Formation role use it when the location is registered: an AWS managed key cannot be used with that role, so alias/aws/s3 is refused with registerLocation, and the plan fails for any key that is not customer managed. 'none': nothing is emitted. fluid verify checks that the key is Enabled and that the objects under the binding's prefix are SSE-KMS with it. On a gcp binding (fluid-schema 0.7.6, BigQuery tables): 'product' creates a Cloud KMS key ring and crypto key for each dataset this product writes (googlekmskeyring fluid-<contract id>-<dataset> and googlekmscryptokey 'bigquery', in the dataset's location, EU and US mapping to the europe and us key locations, rotated every 90 days), grants the project's BigQuery service agent roles/cloudkms.cryptoKeyEncrypterDecrypter on it, and makes it the dataset's defaultencryptionconfiguration and the table's encryptionconfiguration. The project must have the Cloud KMS API (cloudkms.googleapis.com) enabled. A key ring and key cannot be deleted on GCP: tofu destroy removes them from state and schedules the key's versions for destruction (30 days by default), and a later apply adopts the same names. 'projects/<p>/locations/<l>/keyRings/<r>/cryptoKeys/<k>': an existing key in the dataset's location, used as given; its owner grants the BigQuery service agent. 'none': Google-managed keys. An alias/... or arn:... value is refused on a gcp binding, and a Cloud KMS name on an aws binding. Adding a key to a table that exists replaces the table (BigQuery cannot re-key it in place), so fluid apply refuses that plan without --allow-data-loss; the next build lands the data again. fluid verify checks kmsKeyName on the dataset and the table.
NEW in v0.7.6: Maps the contract's LOGICAL principals (accessPolicy.grants[].principal and exposes[].policy.authz.columnRestrictions[].principal, keyed exactly as the contract writes them) to the identities they stand for on this binding's platform, so the base contract stays cloud-neutral and each environment's overlay binds it. On gcp an identity is an IAM member (user:, group:, serviceAccount: or domain:); on aws an IAM principal ARN. A value is one identity or a list; [] says the principal has no identity on this cloud, and nothing is granted to it there. When the block is present, every principal the contract names for this expose must be a key: an unmapped one is refused, never emitted as written. Without the block the principals are used as written, except that a GCP principal in a reserved top-level domain (.example, .test, .invalid, .localhost) is refused as a placeholder. Modelled on ODCS v3's roles[] bound per server (servers[].roles) and on dbt grants resolved per target.
+
+
+
+
-
-
-
-
-
+
-
+
-
+
-
-
+
+
Each additional property must conform to the following schema
NEW in v0.7.6: An expose's lifecycle. The same fields as the contract-root lifecycle, plus expire, which is only meaningful where there is storage to expire.
How long this expose's data is kept (ISO-8601 duration, e.g. P30D, P7Y). A declaration unless expire is true. With expire: true on an AWS S3 binding, fluid apply expires the objects under the binding's prefix this long after they are written; on a GCP BigQuery table it deletes each daily partition this long after its day ends. Years and months are counted as 365 and 30 days, and a part of a day rounds up to a whole one.
NEW in v0.7.6: Delete stored data once it is older than retention. Default false, so a contract that declares a retention period does not start deleting data when forge-cli is upgraded. Honoured by the AWS emitter on an S3 binding and by the GCP emitter on a BigQuery table; on a gcp binding that is not a BigQuery table (GCS, Pub/Sub, Iceberg storage) it is refused rather than dropped. AWS: fluid apply writes one rule per expose into the bucket's awss3bucketlifecycleconfiguration, filtered to the binding's prefix (location.path, else {database}/{table}/; a binding with neither is refused, so no rule ever expires a whole bucket). The rule expires the current version retention after it is written, removes a noncurrent version (on a versioning-enabled bucket) one day after it stops being current, and aborts an incomplete multipart upload after 7 days or the retention period if shorter. The lifecycle configuration is AUTHORITATIVE for the whole bucket: S3 keeps one per bucket, so rules that fluid apply did not write are replaced. It is therefore written only for a bucket this product owns; on a shared (pool) bucket (packaging) nothing is written, the pool's owner must hold the rule, and fluid verify checks that an enabled rule covering the prefix expires objects after exactly this period. Objects already older than the period when the rule is first applied are expired on S3's next lifecycle run, typically within a day. Removing expire later deletes the lifecycle configuration, which fluid apply refuses without --allow-data-loss (it refuses every plan that removes a resource holding data or policy; removing an access grant or a policy tag is a revocation and is not gated); tofu destroy deletes it. GCP (fluid-schema 0.7.6): the BigQuery table is partitioned by day (timepartitioning type DAY, on binding.location.partitionBy when it names one date or timestamp column, else by ingestion time) with expirationms = retention in days, so BigQuery deletes each partition that long after its day ends. BigQuery counts from the partition's date, not from when its rows were written: by ingestion time (no partitionBy, the default and the S3 rule's semantics) no row is deleted sooner than retention after it landed; with partitionBy, retention is the age of the date in that column, so a backfill of rows whose date is already older than retention lands in expired partitions and is deleted at once. It is never a whole-table expiration, which would delete the product. BigQuery cannot partition a table that exists, so adding expire to a live table replaces it: fluid apply refuses that plan without --allow-data-loss, and the next build lands the data again. A later change of retention is an in-place update. Removing expire from a live table removes the partitioning's replacement trigger, which fluid apply also refuses without --allow-data-loss; BigQuery cannot un-partition a table, so to keep data longer set a longer retention instead. fluid verify checks the live table's partition expiration.
NEW in v0.7.2: Semantic model definition mapping physical columns to business concepts. Defines entities, measures, dimensions, and metrics for agent-consumable business logic. Eliminates semantic hallucination by providing machine-readable KPI definitions. Compatible with dbt MetricFlow, Snowflake Semantic Views, and OSI-aligned formats.