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

# Changelog

> Version history for the Midplane open-source engine, newest first.

User-facing highlights for each release of the Midplane engine. For the full version-by-version detail — including breaking changes, audit schema migrations, and the reasoning behind each change — see [`CHANGELOG.md`](https://github.com/midplaneai/midplane/blob/main/engine/CHANGELOG.md) in the repo.

<Update label="0.15.0" description="2026-07-22">
  **Self-signed and private-CA Postgres connect on `sslmode=require`.** The engine now follows libpq's TLS semantics instead of node-postgres's stricter default: `sslmode=require` (and `prefer`) **encrypts without verifying** the certificate, so a database presenting a self-signed or private-CA cert connects rather than failing with an opaque `self signed certificate` the moment an agent runs its first query. Supplying a root CA (`sslrootcert=…`) still verifies against it, `verify-ca` / `verify-full` are unchanged, and `disable` turns TLS off. A DSN with no `sslmode` behaves exactly as before.

  See [connecting to your Postgres](/docs/resources/troubleshooting#connecting-to-your-postgres).
</Update>

<Update label="0.14.0" description="2026-07-01">
  **Column masking moves to a source-side query rewrite.** Masking is now applied by rewriting the query *before* it runs, instead of redacting the result after execution. This fixes the blast radius of the earlier approach: a mask on one column no longer blanket-denies every aggregate or computed query, so `count(*)`, joins, and `GROUP BY` over a masked table run normally. It also **closes the filter-inference bypass** — a predicate or sort on a masked column now operates on the masked value. Shapes the engine can't safely rewrite — views, whole-row serialization, opaque functions — still fail closed with a [`column_masking`](/docs/reference/denial-reasons) reject, now *before* the query runs. On Cloud this is on for every masked database; self-host opts in with `MIDPLANE_MASK_SOURCE_REWRITE=1` (or per database, `mask_source_rewrite: true`).

  See [column masking](/docs/concepts/masking).
</Update>

<Update label="0.13.0" description="2026-06-24">
  **Expanded the column-masking transform catalog.** On top of 0.12.0's `full-redact` and `consistent-hash`, five more transforms ship: `null-out` (type-preserving `NULL`), `partial` (reveal a prefix/suffix — this subsumes the retired `keep-last-4`), `generalize` (truncate a date or bucket a number), `pseudonymize` (deterministic realistic fakes — email, name, phone), and `noise` (proportional numeric jitter, the only non-deterministic transform). Each parametric rule declares the column type it accepts and **fails closed** — applying a transform to an out-of-domain column rejects the whole result set rather than silently passing it through.

  **`SELECT … INTO` now denies under a read-only policy.** It's the legacy spelling of `CREATE TABLE … AS SELECT`, so it's governed as a write to the new table — a read-only agent can no longer materialize a table this way. Previously the two spellings parsed differently and `SELECT … INTO` slipped through; they're now indistinguishable to policy.

  See [column masking](/docs/concepts/masking) and [what Midplane blocks](/docs/concepts/table-access#beyond-table-access).
</Update>

<Update label="0.12.0" description="2026-06-19">
  **Column-level dynamic masking.** Redact sensitive *values* in query results, declared per column on top of [`table_access`](/docs/concepts/table-access). The engine resolves each output field back to its true base column by provenance — so aliases, joins, and `SELECT *` are all covered — and applies the transform after execution, before any rows leave the engine. It's **fail-closed**: a result shape it can't prove safe to mask (a view, an aggregate, a whole-row output) is withheld entirely (a [`column_masking`](/docs/reference/denial-reasons) reject), never leaked. Initial transforms: `full-redact`, `consistent-hash`, and `keep-last-4` (the catalog expands in 0.13.0). Masks need a per-deployment `MIDPLANE_MASK_SALT`; the engine refuses to boot a database that declares masks without it.

  See [column masking](/docs/concepts/masking).
</Update>

<Update label="0.11.0" description="2026-06-18">
  **Per-agent least-privilege database scope.** The engine narrows each session to the databases a credential was granted, clamping writes to read-only where the grant says so. This is what lets each agent reach only the databases you allow, at the access you allow — chosen at OAuth consent or token creation.

  See [roles & team access](/docs/cloud/projects#team-roles).
</Update>

<Update label="0.10.0" description="2026-06-12">
  **The CLI grows into the OSS operator surface.** OSS Midplane has no web UI by design — so the `midplane` binary becomes the product UI. Every new command reuses the real enforcement path rather than reimplementing it. Output is human-readable on a TTY and JSON lines when piped (`--json` / `--pretty` to force either; color respects `NO_COLOR`).

  * **`midplane init`** — an interactive setup wizard: introspect the database, pick per-table grants, and write a schema-validated, linted policy file (the DSN is never echoed or written).
  * **`midplane query`** — send one statement through the server exactly as an agent would, with the same policy decision, audit row (`agent_name=midplane-cli`), and deny message. A verification harness, not a database client.
  * **`midplane doctor`** — preflight + smoke checks in boot order, ending in an end-to-end MCP canary that proves the audit pipeline records the call.
  * **`midplane audit denies`** and **`midplane audit show <query_id>`** — "what got blocked and why," and the full event chain for one query.
  * **`midplane policy test --server`** — dry-run against a running server's currently-loaded policy via `/admin/dry-run`, not just a file.

  See the [CLI reference](/docs/reference/cli).
</Update>

<Update label="0.9.0" description="2026-06-11">
  **Dangerous-statement guardrails — on by default.** A new policy rule, [`dangerous_statement`](/docs/reference/denial-reasons), denies categorically-destructive operations *regardless of `table_access`* — so a table you've granted `read_write` can't be turned into a whole-table wipe or a schema change. `block_unqualified_dml` denies a `DELETE`/`UPDATE` with no `WHERE` clause (caught at any AST depth, including inside a CTE); `block_ddl` denies `DROP`/`TRUNCATE`/`ALTER` (`CREATE` is not blocked). Both default on, even with no policy file; opt out per flag in the [`guardrails`](/docs/reference/policy-schema) block.

  **Behavior change (safe-by-default):** upgrading a deployment that marks tables `read_write` will start denying those shapes on those tables. The no-YAML default already denied all writes, so out-of-the-box outcomes are unchanged. See [upgrading](/docs/self-host/deploy#upgrading).
</Update>

<Update label="0.8.0" description="2026-06-10">
  **`POST /admin/dry-run` — test a policy decision without executing.** "Would this SQL be allowed or denied?" answered by the *same* code that enforces policy at query time (parse → classify → `table_access` → guardrails → decide), stopped **before** execution — it never opens a database connection and never runs a statement. Ask with a literal `sql` or with `(table, action)` **probes**; the response carries a verdict, the `matched_rule`, and a `policy_hash`. This powers the Cloud dashboard's policy test panel and [`midplane policy test --server`](/docs/reference/cli#policy-test).

  See [HTTP endpoints](/docs/reference/mcp-tools#http-endpoints).
</Update>

<Update label="0.7.1" description="2026-06-05">
  **Configurable HTTP bind host — `MIDPLANE_HOST`.** The Streamable HTTP transport was hard-bound to IPv4 (`0.0.0.0`); the bind host is now read from `MIDPLANE_HOST`. It defaults to `0.0.0.0`, so self-host and local boot are unchanged — set it to `::` to serve a dual-stack (IPv6 + IPv4) network, such as an IPv6-only private network. See [environment variables](/docs/reference/environment-variables).
</Update>

<Update label="0.7.0" description="2026-06-04">
  **`midplane policy` CLI** — author, trust, and dry-run a policy file without hand-editing YAML blind. Four subcommands on the existing `midplane` binary:

  * `policy init` — scaffold a commented policy file; with `--url`, list every table in the `public` schema (read-only) and emit each under `table_access`. The DSN is never printed or written to the file.
  * `policy validate` — parse the YAML against the same schema the server boots with. Offline-safe: an unset `${VAR}` doesn't fail a structural check.
  * `policy lint` — security-posture findings beyond schema validity (writes granted, policies that restrict nothing). Errors exit nonzero for CI gating.
  * `policy test --sql "..."` — run a query through the real engine against the file's policy and print the decision, the rule, and the exact agent-facing denial message — no database connection.

  **CTE self-shadow read bypass fixed (`table_access`).** A non-recursive CTE's name doesn't bind inside its own body, so `WITH audit_log AS (SELECT * FROM audit_log) SELECT * FROM audit_log` reads the **real** `audit_log` in its body — which must be policy-checked. That body read was previously mistaken for a reference to the CTE itself and skipped, allowing a read of a denied table. Now a CTE's own name is excluded while walking its body; recursive CTEs keep their self-reference, and legitimate shadowing (a body that reads nothing real) still allows.

  See the [policy CLI](/docs/reference/cli#midplane-policy) and [what Midplane blocks](/docs/concepts/table-access#beyond-table-access).
</Update>

<Update label="0.6.0" description="2026-05-20">
  **Per-token audit attribution.** The Cloud proxy injects an `X-Midplane-Token-Id` header on every forwarded request, naming the token that opened the session. The engine stamps `mcp_token_id` on every audit row from that session — so customers issuing multiple named tokens per connection (one for CI, one for an interactive analyst) can attribute every query to the token that authorized it. The column is additive and nullable; self-host and non-MCP callers read NULL.

  See [tokens](/docs/cloud/projects#machine-tokens) and the [audit events reference](/docs/reference/audit-events).
</Update>

<Update label="0.4.0" description="2026-05-02">
  **Structured per-call intent.** `intent` is now a required structured field on the `query` tool's input schema. Agents fill it the same way they fill `sql`, validated by the MCP SDK before the engine runs — so every audit row carries an LLM-supplied "why" (observed fill rate: 100% on stock Claude Code traffic). This replaced three earlier resolution channels (MCP `_meta`, SQL comment hint, HTTP header) that no off-the-shelf agent actually populated.

  See the [audit trail](/docs/concepts/audit-trail).
</Update>

<Update label="0.3.0" description="2026-05-01">
  **Agent identity on every audit row.** The MCP `initialize` handshake already sends `clientInfo: { name, version }`; the audit row now stores them as separate `agent_name` and `agent_version` columns (rather than one combined string that would kill grouping and filtering). No agent action required — every MCP client already sends `clientInfo`.

  See the [audit events reference](/docs/reference/audit-events).
</Update>

<Update label="0.2.0" description="2026-04-30">
  **Multiple databases.** One `midplane/midplane` container can now serve N databases through a single MCP endpoint. Configure them under a top-level `databases:` block, each with its own `url` and `table_access`. `${ENV_VAR}` interpolation keeps DSNs out of YAML files.

  **Dynamic tool surface.** With two or more databases, `query` and `describe_table` gain a required `database` arg, `list_tables` gains an optional one (omitted = fan out across all databases), and a new `list_databases` tool reports each database's policy summary. Single-database setups see no change. Every audit row is tagged with the originating database name.

  See [multiple databases](/docs/policies/overview#serve-multiple-databases).
</Update>

<Update label="0.1.0" description="2026-04-29">
  **First tagged release.** Three policy rules — `table_access`, `multi_statement`, and `parse_error` — sit on the audit-before-execute pipeline, pinned by an [adversarial SQL corpus](/docs/security/adversarial-corpus) covering CTE-hidden writes, stacked-statement injection, parser edges, and exec side-effects. **100% line coverage** on the policy surface.

  Ships as `@midplane/engine` (the embeddable pipeline) and `@midplane/mcp-server` (three MCP tools over stdio and Streamable HTTP), plus a multi-arch, non-root production Docker image. Agent compatibility verified across Cursor, Claude Code, and Claude Desktop.

  * Per-table read/write policy via YAML, with schema-qualified keys and `default: read`.
  * `COPY` and `LOCK` deny unconditionally — side effects the per-table YAML can't grant.
  * Audit-before-execute ordering: if the pre-execute audit write fails, the query never runs.
  * `midplane audit` CLI (`tail | stats | since`) to read the audit log without writing SQL.
  * Anonymous, aggregate telemetry, disabled with `MIDPLANE_TELEMETRY=0`.
</Update>

## Next steps

<Columns cols={2}>
  <Card title="Upgrading" icon="arrow-up" href="/docs/self-host/deploy#upgrading" horizontal>
    How to move a self-host instance between versions, including audit schema migrations.
  </Card>

  <Card title="Contributing" icon="git-pull-request" href="https://github.com/midplaneai/midplane/blob/main/CONTRIBUTING.md" horizontal>
    The dev setup and the highest-leverage contribution.
  </Card>
</Columns>
