Skip to main content

Unreleased

What has landed on main since RC4, regenerated on every build of the site: the in-progress app notes and REST API 1.303.0 to 1.339.1 (29 versions).

What's new in the app​

From the in-app What's new for Apiome 08-2026 RC5.

Features/Improvements​

  • API Formats:
    • Hardening import functionality
    • Adds formats:
      • MCP import and export
      • Kong import and export
      • Arazzo 1.1 import and export
      • AsyncAPI 2.x export
      • OData v2/v3 import and export
      • WSDL 2.0 import and export
      • Avro IDL (.avdl) import and export
      • Swagger 1.2 import
      • Postman Collection v2.0 import
      • Protobuf editions 2023/2024 normalization parity
      • RELAX NG import
      • DTD import
      • Schematron import
      • CDDL (RFC-8610) import and export
      • Apache Arrow/Flight import
      • Open Data Contract Standard (ODCS v3.1) import and export
      • Kafka Connect schema import and export
      • dbt model and semantic-manifest import
      • SQL DDL import
    • Linting rule pack updates for scoring new import types
      • Data contracts are now scored on their own terms: ownership, service levels, freshness, retention, column documentation, row identity, classification and declared quality checks
    • Added pills to supported format list to show the source and type that the API format provides
    • Every format now declares which versions it reads and writes, and which version an export produces by default
  • Mock Services:
    • Several mock services have been improved including mock rules and testing via UI and JSON rules
  • Documentation:
    • The user guide is now a searchable documentation site, grouped by job; Help & docs links open it
    • Docs pages show product screenshots in light and dark, retaken from the product every week
    • A new Getting started guide walks from sign-in to a published, browsable spec
    • Full release notes at the foot of this dialog opens a post per release on the documentation site, with every REST API change and links to the issues

Bug Fixes​

  • Import:
    • Fixed upload to accept all file format extensions instead of just the 10 it had previously
    • Fixes dependency for AsyncAPI to re-enable import functionality
    • Fixing durability of import process, added extra tests to REST service test suite
    • Fixing bulk import functionality:
      • Now includes the ability to bulk import into existing or new projects without having to validate all selections
      • Visual indicator of the bulk import is now implemented properly
  • MCP:
    • Hardens tool array emit for LLM tool summary

REST API​

1.339.1 — 2026-10-07​

Added​

  • Generated REST reference on the documentation site (#5628, DOCS-1.11): scripts/generate_rest_reference_docs.py renders openapi.yaml into apiome-docs/docs/reference/rest/ — one page per tag (plus untagged) with every operation's parameters, request body, responses and the schemas it uses. The index records the document's SHA-256 (openapi_sha256), and yarn docs:check fails when openapi.yaml changes without regenerating; --check and tests/test_rest_reference_docs.py fail on stale pages. Rendering lives in app.rest_reference_doc. No API change; the OpenAPI version moves to 1.204.1.

1.339.0 — 2026-10-07​

Changed​

  • Guide pages moved to the documentation site (#5619, DOCS-1.2): every docsPage the API returns now names a page of the Docusaurus site instead of the retired docs/guide/ folder — e.g. GET /v1/lint/rules → "docsPage": "apiome-docs/docs/build/lint-rules.md". The value is still a monorepo-relative source path; apiome-ui resolves it to https://apiome.github.io/apiome/build/lint-rules. Affected fields: lint-rule catalog and style-guide docsPage, blocking-rule docs_page (schema → build/lint-rules, MCP → govern/mcp-*-rules), axis algorithmDocsPage (build/axis-score) and import-preflight rule links. Blocking schema-rule reference URLs now point at the site (https://apiome.github.io/apiome/build/lint-rules#<rule>).
    • New app.docs_site builds those paths, the site URLs and the pages' front matter.
    • scripts/generate_lint_rule_docs.py, generate_supported_formats_doc.py and generate_format_counts.py write into apiome-docs/docs/**; generated pages open with front matter and mark rule / format anchors as heading ids (### rule.id {#rule-id}).
    • OpenAPI info.version 1.204.0 (description text only).

1.336.0 — 2026-10-06​

Added​

  • Agent toolset description enrichment (#4531, AGX-1.3): flags tools an agent will struggle with, and lets the copilot propose better descriptions that a person reviews before any agent sees them.

    curl -sX POST "$APIOME/v1/tenants/acme/agent-toolsets/$TOOLSET/enrichment" -H "Authorization: Bearer $JWT"
    curl -sX PATCH "$APIOME/v1/tenants/acme/agent-toolsets/$TOOLSET/enrichment/$PROPOSAL" \
    -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' -d '{"decision": "accept"}'
    curl -s "$APIOME/v1/tenants/acme/agent-toolsets/$TOOLSET/compiled" -H "Authorization: Bearer $JWT"
    • Agent-hostile flags with machine-readable reasons: missing-description, thin-description, missing-examples, undocumented-errors, missing-parameter-description, thin-parameter-description. They feed AGX-4.4.
    • Copilot proposals from an Ollama chat model (APIOME_AGENT_ENRICHMENT_MODEL; unset means flag-only), written only from the spec's own documentation. The pass is idempotent and asks about at most 20 operations per run.
    • Human review. accept (optionally edited) or reject. Only accepted text is compiled, and only while the toolset's new descriptionEnrichment setting is on (the default). V273 adds agent_toolset_enrichments; its CHECK makes accepted text without a review unrepresentable.
    • GET …/compiled returns the toolset as agents are served it.
    • Audited as agent.toolset.enrichment.run / agent.toolset.enrichment.review (metadata only).
    • New app.ollama_chat: the stdlib Ollama chat client for REST-side copilot features.
    • Docs: docs/agent_toolset_enrichment.md.

1.333.0 — 2026-10-05​

Added​

  • Agent toolsets: tool selection & curation (#4530, AGX-1.2): a tenant decides which operations of a published version its AI agents may call as MCP tools. Safe by default: reads on, write operations opt-in, each with an explicit confirmation.

    curl -sX POST "$APIOME/v1/tenants/acme/agent-toolsets" \
    -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
    -d '{"versionId": "'"$VERSION"'"}'
    curl -sX PATCH "$APIOME/v1/tenants/acme/agent-toolsets/$TOOLSET/tools/$TOOL" \
    -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
    -d '{"enabled": true, "confirmWriteOp": true}'
    • Two tables (V270). agent_toolsets: one per published version, with enabled and target (prod | mock, consumed by AGX-2.4). agent_toolset_tools: one row per callable operation, holding the canonical operation key, the AGX-1.1 compiled tool name, write_op, enabled, and who confirmed an enabled write op and when.
    • Seeding. Creating a toolset enables GET/HEAD operations and GraphQL queries (deprecated ones excepted). Every other operation is a write op and starts disabled. Enabling one without confirmWriteOp: true is a 422 agent-toolset-write-op-unconfirmed, and a database CHECK makes an enabled but unconfirmed write op unrepresentable.
    • Seven routes under /v1/tenants/{t}/agent-toolsets: list (?versionId), create, get (with tools), PATCH (enabled / target), DELETE, GET …/{id}/tools, and PATCH …/{id}/tools/{toolId}. Guarded by the existing api_keys permissions; no new RBAC resource.
    • Audited. agent.toolset.create, agent.toolset.update (before/after), agent.toolset.delete and agent.toolset.tool.update (operation, write-op flag, enabled before/after, confirmation time), with metadata only.
    • Docs: docs/agent_toolsets.md.

Changed​

  • AGX-2.2 / AGX-3.1 handoffs closed. V270 deletes upstream credentials and agent keys whose toolset does not exist, then gives both toolset_id columns a (tenant_id, toolset_id) foreign key to agent_toolsets with ON DELETE CASCADE. Deleting a toolset now deletes its upstream credentials and agent keys. POST /agent-keys and the upstream-credential list/create routes answer 404 agent-toolset-not-found for a toolset that is not in the caller's tenant.
  • apiome-mcp's agent-access middleware now reads a key's enabled tools from the toolset by default (toolset_enabled_tools), instead of failing closed on every request.

1.332.0 — 2026-09-17​

Added​

  • Agent keys (#4537, AGX-3.1): a new kind of API key for AI agents. Each one is bound to one agent toolset, limited to an explicit tool allowlist, and can expire. It is the credential an agent presents to the MCP agent runtime, and the identity that quotas (AGX-3.2) and usage analytics (AGX-3.3 / 3.4) will attach to.

    curl -sX POST "$APIOME/v1/tenants/acme/agent-keys" \
    -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
    -d '{"name": "claude-desktop", "toolsetId": "'"$TOOLSET"'",
    "toolAllowlist": ["listPets", "getPetById"], "expiresAt": "2026-12-31T00:00:00Z"}'
    • api_keys extended (V269). kind (workspace | agent; existing rows are workspace), toolset_id and tool_allowlist (a JSON array of AGX-1.1 tool names, at most 1024, no wildcard). expires_at is reused. CHECKs keep the two kinds exclusive and pin agent keys to the scope agent:invoke.
    • Five routes under /v1/tenants/{t}/agent-keys: list (?toolsetId, ?includeRevoked), create, get, PUT …/{id}/allowlist, and DELETE …/{id} (revoke, idempotent). Guarded by the existing api_keys view / create / edit / delete permissions; no new RBAC resource.
    • Secret shown once. The secret is ak_ + 64 hex characters, returned only by create. Rows store a bcrypt hash and the usual lookup prefix; no response carries the hash.
    • Audited. agent.key.create, agent.key.allowlist_update (the list before and after) and agent.key.revoke go to the access audit, with metadata only.
    • Enforced in apiome-mcp. apiome_mcp.agent_access.AgentAccessMiddleware narrows tools/list and tools/call to the toolset's enabled tools ∩ the allowlist. A revoke, expiry or allowlist edit applies to the agent's next request. Until AGX-1.2 (#4530) supplies toolset curation, it fails closed.
    • toolset_id has no foreign key yet (agent_toolsets is AGX-1.2). Docs: docs/agent_keys.md.

Security​

  • An agent key never authenticates a REST call. validate_api_key now selects kind = 'workspace' keys only, and falls back to the older queries on a pre-V269 database. Separately, no REST scope-allowlist entry accepts agent:invoke, so an agent key would get a 403 on every route even if the kind filter were bypassed.

1.331.0 — 2026-09-17​

Added​

  • Upstream auth vault (#4534, AGX-2.2) — agents must never hold real API credentials. A tenant now stores the upstream credential a managed MCP toolset presents to its real API, and the AGX-2.1 invocation proxy injects it server-side; the agent only ever holds its Apiome key.

    curl -sX POST "$APIOME/v1/tenants/acme/agent-toolsets/$TOOLSET/upstream-credentials" \
    -H "X-API-Key: $APIOME_KEY" -H 'Content-Type: application/json' \
    -d '{"serverUrl": "https://api.example.com/v1", "kind": "apiKey",
    "in": "header", "name": "X-Api-Key", "secret": {"value": "sk_live_…"}}'
    • Encrypted at rest. apiome.upstream_credentials (V268) holds ciphertext only, sealed by the shared envelope cipher under its own key map (APIOME_UPSTREAM_CREDENTIAL_ENCRYPTION_KEYS) and vault magic. No key configured ⇒ storing is a 503 and nothing is written in the clear.
    • Write-only. GET lists metadata (binding, kind, placement, key version, whether it still opens, created / rotated / last-used); create, rotate (POST …/{id}/rotate) and delete accept a secret and never return one. A test sweeps every GET route in the app for any rendering of a stored secret or its ciphertext. The router's 422s no longer echo a submitted body (app.redacted_validation_route).
    • Bound to a toolset and a server URL. https:// origin plus base path, stored normalized; a secret is only opened for a request with the same scheme, host and port whose path sits under the base path — never for a look-alike host, a downgrade, userinfo or an encoded traversal. Kinds: apiKey (header or query, configurable name), bearer, basic.
    • Rotation without downtime. One in-place UPDATE; concurrent resolves see the old secret or the new one, never neither, and in-flight invocations keep the secret they opened.
    • Use is audited as metadata only in the write-once upstream_credential_uses ledger (which credential, which toolset, when, injected / unavailable); create / rotate / delete land in the access audit as agent.upstream_credential.*. A bound credential that won't open fails the call closed.
    • Guarded by the existing api_keys permission (view / create / edit / delete); no new RBAC resource. toolset_id gains its foreign key in AGX-1.2 (#4530). See docs/upstream_credential_vault.md.

1.329.0 — 2026-09-17​

Added​

  • API change check suite (#4740, GNC-3.1) — a pull request that changed an API used to collect a wall of checks (lint, diff, breaking-change gate, contract test, SDK build), each with its own log, and a reviewer who read none of them. The platform already knew every answer; the suite turns them into one verdict — pending, pass, fail or skipped — recorded as an evaluation, reported on the pull request as the apiome/api-change check (replacing the pending check GNC-2.2's webhook seeded), and optionally required before publish.

    curl -sX POST "$APIOME/v1/tenants/acme/projects/pets/versions/2.0.0/check-suite" \
    -H "X-API-Key: $APIOME_KEY" -H 'Content-Type: application/json' \
    -d "{\"commit_sha\": \"$GITHUB_SHA\", \"pr_number\": 42}"
    • Five components, all existing evidence. lint (the stored-first lint report; error-severity violations fail it, as they fail publish), breaking and consumers (the CTG classification against the previous published revision and CTG-4.2's per-consumer verdicts for that same diff), contract (the newest ECA contract run of this revision, counted only while recompiling its own reference still yields the digest it executed) and sdk (the SDK kit manifest built from the draft). The three CTG-4.5 components are judged by the deploy gate's own evaluators and thresholds, so the pull request and the deploy gate cannot disagree.
    • Deterministic. A tenant (or project) policy marks each component required, advisory or off; the required ones reduce with GNC-2.2's precedence (a failure fails, an unfinished component holds, a set of skips is a skip). Missing evidence holds a required component pending and skips an advisory one; unreadable evidence is pending, never green; a version with no captured source skips contract and sdk rather than waiting forever.
    • The suite judges the draft. Its verdict is reported against the commit the binding is synchronized with. A newer commit gets a placeholder: skipped (spec-unchanged) when it does not touch the bound specification, otherwise pending (draft-not-synchronized).
    • Idempotent re-runs. An evaluation is keyed by the draft digest, commit, requirements, thresholds, and every component's verdict and evidence; unchanged inputs are a 200 replay of the same evaluation with the same evidence ids, and the provider is not called again.
    • Drill-down. GET …/check-suite/runs/{id} names every component's evidence (ids, digests, counts), a link to it, and the rule behind it, plus snapshots of both policies. The pull-request summary is the same thing as markdown. Consumer names are redacted for readers without consumer_contracts:view.
    • Required before publish. requiredForPublish on the suite policy refuses (422, apiCheckSuiteGate) a version whose current content has no passing evaluation under the component requirements and thresholds in force; force-publish stays the escape, and every judged publish is audited as version.api_check_suite_gate.
    • Endpoints: POST/GET …/projects/{project}/versions/{version}/check-suite, GET …/runs, GET …/projects/{project}/check-suite/runs/{run_id}, and GET/PUT/DELETE of …/governance/check-suite-policy and …/projects/{project}/check-suite-policy (tenant administrators only for changes). The latest-evaluation read is allowlisted for diff:read / lint:read CI keys. CLI: apiome checks run|show.
    • apiome-db V267: apiome.api_check_suite_policy (one row per scope, mutable — every evaluation snapshots the policy it was judged under) and apiome.api_check_suite_runs (append-only except the ON DELETE SET NULL of its author; a placeholder can never pass or fail), plus an index on ECA-1.3 runs by source->>'revision_id'.

Fixed​

  • A retried provider check publish that succeeded was never recorded (GNC-2.2). The publish ledger was unique on (check_run_id, request_fingerprint), so a verdict's failed attempt took the slot its successful retry needed: the retry reached the provider, collided in the ledger, and never wrote the provider's check-run id back — so every later publish POSTed a new check run onto the pull request. V267 re-keys the ledger per outcome and the lookup reads a verdict's dispatch first; a verdict is still dispatched at most once.

1.328.0 — 2026-09-16​

Added​

  • Three-way spec synchronization (#4739, GNC-2.3) — a bound draft has three descriptions of the same API — the repository at the commit it is synchronized with (the base), the repository at the commit its branch has moved to (Git), and the version as this platform has it (the draft) — and they drift apart independently. Copying either repository side over the draft destroys whatever somebody was editing and invalidates whatever reviewers already decided. A semantic three-way merge now measures both sides against the base, applies the incoming changes that touch nothing the draft touched, and hands back every overlap as an explicit conflict.

    # The branch moved. What would land, and what collides?
    curl -sX POST "$APIOME/v1/tenants/acme/projects/pets/versions/2.0.0/binding/sync" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}'

    # Decide one collision. `git` takes the repository's value; `draft` keeps the version's.
    curl -sX POST "$APIOME/v1/tenants/acme/projects/pets/sync-plans/$PLAN/conflicts/$CONFLICT" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"resolution":"git","note":"the rename was agreed in review"}'
    • A merge result is a reading, never a write. There is no code path from this surface to the canonical model, to versions, or to a review — not in the store, not in the accessors, and not in the schema. Computing a merge writes one row; settling a conflict records which side a person chose. Both leave the draft exactly where it was, which is what makes it safe for a webhook to raise a candidate and for anyone at all to merge it.
    • apiome-db V266: apiome.draft_sync_plans (one merge per binding and trio of documents, recording the base, Git and draft digests it was computed from, what merged and how much did not, with the identity frozen by a trigger and a CHECK that keeps the status and the two counts from disagreeing) and apiome.draft_sync_conflicts (one row per overlap, carrying all three values with the repository file and line it lives at, settled once towards git or draft).
    • apiome-rest app.spec_sync (vocabulary, models, the merge engine, the plan fingerprint and the YAML/JSON line locator — all pure), app.spec_sync_store (the rules), app.spec_sync_routes (four endpoints), and the accessors at the end of database.py, whose sync.planned / sync.conflict_resolved audit rows are written inside each write's transaction.
    • Conflicts are located, not just named. Each carries its RFC 6901 pointer, the shared document/path/operation/component/schema grouping every other change list in the product uses, what each side did to the base, all three values, and the repository file, 1-based line and link at the commit. Lines come from composing the incoming document rather than loading it, and a mapping entry reports its key's line — in block YAML the value of summary: starts on the next line, which is not where anybody would look. A value too large to send to a browser is replaced by its size, and a merge that collides in more places than one result holds says so on the row — a result that quietly dropped findings would read as less conflicted than it is.
    • Three proven reads, never a payload. Both commits are fetched through the same credential-resolving read binding uses, so a repository the tenant cannot reach is answered 403 binding-repository-forbidden rather than merged from a guess. And the base is re-read and re-hashed first: if it no longer matches the digest the binding recorded, history was rewritten underneath the merge base and the request refuses with sync-base-drifted instead of guessing which side changed what.
    • Reruns are free, not just idempotent. The rerun key is taken over the binding and the three documents' digests — all known before any network call — so re-merging an unchanged trio returns the stored result without fetching two commits to prove it would be the same. The draft is identified by its content digest rather than its revision id, because an edited revision is a different document; a stored result whose draft has moved on reads as stale.
    • Work that already exists is reported, not trampled. A result carries a guard — none, review_decided (an open review's current round already holds a decision) or version_published. A guard never refuses the merge: knowing exactly what would collide is precisely what such a reader needs.
    • Documented in apiome-rest/docs/spec_sync.md. This ticket deliberately writes nothing back to Git and applies nothing to a draft; the merged document is computed and proven deterministic, but turning it into an edit is a separate, explicit act with no storage here.

1.327.0 — 2026-09-16​

Added​

  • Provider webhook and status adapter (#4738, GNC-2.2) — GNC-2.1 gave a draft a durable relationship to a repository ref, but nothing could answer the provider: a pull request that changed an API showed no verdict from this platform, because there was nowhere to record one and nothing to send it with. Both halves now exist — a normalized check model that belongs to the platform rather than to any provider, and one status adapter interface with GitHub, GitLab and Bitbucket behind it.

    # Record a verdict about the commit this draft is bound to, and put it on the pull request.
    # (binding, commit, name) identifies the check, so the same call twice is one verdict.
    curl -sX POST "$APIOME/v1/tenants/acme/projects/pets/versions/2.0.0/binding/checks" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"name":"apiome/api-change","state":"fail","title":"2 breaking changes",
    "summary":"`GET /pets` lost a required field.","details_url":"https://…"}'

    # What did the provider actually do with it?
    curl -s "$APIOME/v1/tenants/acme/projects/pets/checks/$CHECK_ID" \
    -H "Authorization: Bearer $TOKEN"
    • Four words, provider-independent: pending / pass / fail / skipped. The three providers disagree about what a status even is — GitHub splits it into status and conclusion, GitLab has one commit state, Bitbucket has build states in capitals and no skipped at all — so storing any one of them would make the other two lossy translations. Only pass ever maps to a provider's passing status: a skip is "we did not look", which is not "we looked and it is fine".
    • apiome-db V265: apiome.provider_check_runs (one verdict per binding, commit and check name, backed by a unique index — the re-run idempotency — with completed_at tied to the state by a CHECK and the identity frozen by a trigger) and apiome.provider_check_deliveries (an append-only publish ledger, one row per distinct verdict sent, which may not claim a reach it did not have).
    • apiome-rest app.provider_checks (vocabulary, models, codes, the provider spelling tables and the fingerprint — all pure), app.provider_status_adapter (one StatusAdapter contract; GitHub check runs, GitLab commit statuses, Bitbucket build statuses), app.provider_check_store (the rules), app.provider_check_routes (four endpoints), and the accessors at the end of database.py, whose check.recorded / check.published audit rows are written inside each write's transaction.
    • Recording comes before publishing, always. A verdict is written first and only then offered to the provider, so a refusal appends a failed row to the publish ledger and the verdict stands. A check recorded and not published is evidence; a check published and not recorded is a green tick with nothing behind it.
    • Idempotent twice over. Re-recording the same verdict moves one row rather than fanning out a second, and each publish carries a fingerprint of the verdict — not of the HTTP call, which changes once a provider hands back an id — consulted before the adapter runs. A redelivered webhook therefore costs the provider nothing, not just us.
    • A delivery resolves to an authorized binding, or to nothing. A verified push or PR on a bound ref now seeds a pending check beside GNC-2.1's sync candidate, through the existing REPO-4.3 endpoint and deliberately outside its tracked-branch gate. Authorized means the binding is active and its repository registration still exists — that registration is the credential a verdict is published with. Both halves are best-effort: neither can turn a verified delivery into a 500 the provider would retry forever. The acceptance audit carries checksSeeded.
    • Browser clients never receive repository tokens, and cannot supply one. The token is resolved in server memory from the binding's registration (the same vault lookup an import uses); V265 has no token column on either table, not an encrypted one; the request and response models forbid extras; and a provider's refusal is redacted before it is stored.
    • New settings: APIOME_PROVIDER_CHECKS_ENABLED (publish kill switch — recording is never gated), APIOME_PROVIDER_CHECKS_WEBHOOK_SEED, APIOME_PROVIDER_CHECKS_DETAILS_BASE_URL.
    • Documented in apiome-rest/docs/provider_checks.md. This ticket produces no verdicts — it carries them; the check suite that decides one is GNC-3.1. Making a check required stays a provider-side branch protection setting, which is the repository owner's to make.

1.326.0 — 2026-09-16​

Added​

  • Branch-to-draft binding (#4737, GNC-2.1) — repository import creates a snapshot; it does not create a durable review unit. A binding now pins one draft version to one provider, repository, ref and source path, together with the digest of the source it is synchronized with, and every observed movement of that ref becomes an explicit, auditable sync candidate instead of quietly rewriting the draft.

    # Bind a draft to a branch. The ref is resolved and the selection read through a STORED
    # credential first: that read is the authorization check, and it produces the commit and digest.
    curl -sX POST "$APIOME/v1/tenants/acme/projects/pets/versions/2.0.0/binding" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"repository_id":"…","ref":"main","path":"spec/openapi.yaml"}'

    # Has the branch moved? A moved ref records a candidate; nothing about the draft changes.
    curl -sX POST "$APIOME/v1/tenants/acme/projects/pets/versions/2.0.0/binding/check" \
    -H "Authorization: Bearer $TOKEN"

    # Decide: `applied` re-reads the source at that commit and advances the binding's base;
    # `dismissed` leaves it exactly where it is. A candidate settles once.
    curl -sX POST "$APIOME/v1/tenants/acme/projects/pets/versions/2.0.0/binding/candidates/$ID" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"status":"dismissed","note":"not ours to take"}'
    • apiome-db V264: apiome.draft_repository_bindings (one active binding per draft, backed by a partial unique index; released rows kept as history and frozen by a trigger) and apiome.draft_binding_sync_candidates (idempotent per target commit and per provider delivery, settling exactly once). What a binding names never changes — only the synchronized pair (commit_sha, source_digest) moves, and only while it is active.
    • apiome-rest app.draft_bindings (vocabulary, models, codes, the digest — all pure), app.draft_binding_store (the rules), app.draft_binding_routes (seven endpoints), and the accessors at the end of database.py, whose binding.* audit rows are written inside each write's transaction.
    • Authorization is a proven read, not a claim. Binding, checking, and applying each resolve a stored linked-account credential and actually read the ref and the selection through the provider before anything is written. No token is ever accepted from a request body.
    • A ref update never rewrites a draft. A verified provider delivery on a bound ref raises a candidate through the existing REPO-4.3 webhook endpoint — deliberately outside its tracked-branch gate, because a bound branch need never have been imported from. A push moves the branch it names; a pull request moves its head branch, never its base.
    • De-registering a repository releases every binding it authorized, in the same transaction as the soft delete, reason repository_removed.
    • Database._insert_review_audit was generalized to _insert_workflow_audit_tx; COL-2.1's five call sites are unchanged in behaviour.
    • Documented in apiome-rest/docs/draft_bindings.md. Writing back to the provider (check runs, PR status) is GNC-2.2; merging repository changes into a draft is GNC-2.3, which will settle the same candidates once it can reconcile without silent overwrite.

1.325.0 — 2026-09-15​

Added​

  • Notification model & fan-out (#4521, COL-3.1) — the collaboration roadmap stops depending on people polling pages. Every mention, review request, decision, thread resolution and publish now writes one inbox row per person it concerns, in the same transaction as the event itself, and three endpoints read that inbox.

    # The bell badge
    curl -s "$APIOME/v1/tenants/acme/notifications/unread-count" -H "Authorization: Bearer $TOKEN"
    # {"total":3,"by_type":{"mention":2,"review_requested":1,"review_decision":0,…}}

    # The list behind the notification centre
    curl -s "$APIOME/v1/tenants/acme/notifications?unread=true&limit=20" -H "Authorization: Bearer $TOKEN"

    # Clearing it
    curl -sX POST "$APIOME/v1/tenants/acme/notifications/read" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"all": true}'
    • Storage (apiome-db V263): notifications — recipient, tenant, type, payload jsonb, read_at, plus the actor and the project/version the row points at, so a deleted destination takes its dead links with it while "somebody mentioned you" survives that somebody leaving.
    • Five event types: mention (an edit notifies only the members it newly names), review_requested (every round's reviewers), review_decision (the member who asked for the review), thread_resolved (everyone who took part), version_published (the version's collaborators — its review and thread participants). Nobody is ever notified of their own action, and a recipient set is bounded at 200.
    • Transactional fan-out: each write accessor takes a notify callable and runs it inside its own transaction, so a committed event always has its rows and a refused one leaves none. The insert joins users, so a recipient deleted mid-flight is skipped rather than rolling the event back.
    • Retention: a statement-level trigger keeps each inbox to its newest 500 rows, so the bound holds for every writer rather than depending on each one to remember.
    • API: GET /v1/tenants/{tenantSlug}/notifications (filters unread, type, paged), GET …/notifications/unread-count (total and per type, zeroes included), and POST …/notifications/read (ids or all, idempotent, answering with the new count). All three need only authentication — an inbox is the caller's own, which no resource:action can express — and listing never marks anything read.
    • Documented in docs/notifications.md. Per-type delivery preferences are not stored yet; COL-3.2 needs them and should filter on read, not at write time.

1.324.0 — 2026-09-15​

Added​

  • Approval policy & publish gate (#4519, COL-2.3) — review outcomes become binding at publish time. A tenant can require n approvals (optionally from a named role) before a draft version may be published; publishing without them is refused, and the force-publish escape is audited.

    # Arm the gate on the governing style guide
    curl -sX PUT "$APIOME/v1/style-guides/acme/$GUIDE_ID/policy" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"requiredApprovals": 2, "requiredReviewerRole": "release-manager"}'

    # A publish that has not collected them is refused with 422 + the verdict
    curl -sX POST "$APIOME/v1/versions/acme/pets/$REVISION/publish" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"shortMessage": "Ship it"}'
    • Policy (apiome-db V262): style_guides.required_approvals (0 = off, the default, capped at the 20-reviewer limit) and style_guides.required_reviewer_role (a roles.slug, nullable). Both resolve through the GOV-1.4 guide chain — project assignment → tenant assignment → tenant default — beside the CTG-3.4 breaking-publish level, and both are frozen into each style_guide_revisions snapshot. Read/written at GET/PUT /v1/style-guides/{tenantSlug}/{guideId}/policy, where requiredReviewerRole is the one field for which omitted (leave alone) and null (clear) differ.
    • Gate: only the version's open review and only its current round count. Blocked with a stable reason for no-review, changes-requested, spec-changed (the content moved since the round was requested, so its approvals are stale — COL-2.1's spec_changed), insufficient-approvals, and missing-required-role. A reviewer's role is their effective RBAC slug, so a tenant administrator resolves to owner.
    • 422 contract: {"detail": {"message": …, "approvalGate": {…}}}, the same shape the breaking-publish guardrail and the verification policy use. Force-publishing (skipPublishChecks + forcePublishReason) gets past it, exactly as it does for them.
    • Audit: every publish an armed policy judged appends a version.approval_policy_gate workflow_audit row with action of satisfied, forced, or unavailable — passing verdicts included, so the trail can answer "who signed off on this release?".
    • Faults degrade to unavailable and never block a publish; the degradation is audited rather than silent.
    • Documented in docs/approval_publish_gate.md; 46 tests in tests/test_approval_publish_gate.py.

1.322.0 — 2026-09-14​

Added​

  • Comment anchor resilience (#4516, COL-1.4) — a comment thread no longer silently disappears when its element is deleted, and renames and moves are proven to keep it in place.

    # Threads whose element was deleted, with the element's last-known label
    curl -s "$APIOME/v1/tenants/acme/projects/pets/comment-threads?version=1.0.0&status=orphaned" \
    -H "Authorization: Bearer $TOKEN"

    # Re-attach one to another element of the same version
    curl -sX POST "$APIOME/v1/tenants/acme/projects/pets/comment-threads/<thread id>/relink" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"anchor_type": "property", "anchor_id": "<class property id>"}'
    • Orphaning (apiome-db V260): status gains orphaned, with anchor_label (the element's name when it was deleted — Customer, Customer.email, /customers/{id}, GET /customers/{id}) and orphaned_at. Database triggers orphan threads on every delete path: the class, property, path, and operation DELETE routes, a whole-version source-change rewrite, and foreign-key cascades (a class takes its properties' threads, a path its operations'). A resolved thread keeps its resolution while orphaned.
    • Relink: POST …/comment-threads/{thread_id}/relink {anchor_type, anchor_id} re-attaches an orphaned thread to an element of its own version (or to the version). It comes back resolved if it was, open otherwise. 404 comment-anchor-not-found for a target outside the version, 409 comment-thread-not-orphaned for a thread that is not orphaned. Requires projects:view.
    • Resolving or reopening an orphaned thread is 409 comment-thread-orphaned. Replies still work.
    • Renames and moves (class, property, path, operation, canvas position) are pinned by tests as in-place updates keyed by the element id, and no orphan trigger watches a name column.
    • Documented in docs/comments.md.

1.321.0 — 2026-09-14​

Added​

  • Comment threads (#4513, COL-1.1) — discussion now lives next to the specification instead of in Slack screenshots. Open a thread on a class, property, path, operation, or a whole version; reply in Markdown; mention teammates; resolve and reopen.

    # Ask about a class on version 1.0.0 and mention a teammate
    curl -sX POST "$APIOME/v1/tenants/acme/projects/pets/comment-threads" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"version": "1.0.0", "anchor_type": "class", "anchor_id": "<class id>", "body": "Nullable, @bob.brown?"}'

    # Everything open on that version that mentions me
    curl -s "$APIOME/v1/tenants/acme/projects/pets/comment-threads?version=1.0.0&status=open&mentions_me=true" \
    -H "Authorization: Bearer $TOKEN"
    • Storage (apiome-db V259): comment_threads (tenant, project, version, anchor_type + anchor_id, status, resolution stamps, last_activity_at) and comments (thread_id, author_id, Markdown body, mentions uuid[], edited_at). A thread is anchored by the element's primary key, never canvas coordinates, and the element must exist in the named version when the thread is opened.
    • Endpoints under …/projects/{project_ref}/comment-threads: list (filters version, status, anchor_type, anchor_id, mentions_me; paged with a total; each row carries its opening comment), open, read, delete, resolve, reopen, and …/comments to reply, edit, and delete. Deleting a thread's last comment deletes the thread.
    • Permissions: no new RBAC resource. projects:view is enough to read, comment, resolve, and reopen. Editing or deleting a comment is limited to its author or a tenant administrator, and deleting a thread to whoever opened it or a tenant administrator (403 comment-forbidden).
    • Mentions are resolved on the server against active and pending members, by full email, email local part, or display name without spaces. A handle shared by several members resolves to nobody. Code, escapes, addresses, and URLs are never mentions, and an edit re-resolves them.
    • Rate limit: opening and replying share a per-user budget, APIOME_COMMENT_CREATE_RATE_LIMIT_PER_MINUTE (default 30); over it returns 429 comment-rate-limited. Documented in docs/comments.md.

1.320.0 — 2026-09-10​

Added​

  • Auto-regen on publish (#4497, SDK-4.3) — watch mode for SDKs. Subscribe a project's SDK once, and every published version regenerates it and ships it: an SDK-4.1 registry publish, an SDK-4.2 pull request, or both. Nobody has to remember.

    # Every publish of widgets: publish the npm SDK, then open a PR carrying that same version
    curl -sX PUT "$APIOME/v1/projects/acme/widgets/sdk-regen-subscriptions/npm" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"deliveryMode": "registry_and_git"}'

    # What each publish did — and the dead letter
    curl -s "$APIOME/v1/projects/acme/widgets/sdk-regen-runs?status=dead_letter" -H "Authorization: Bearer $TOKEN"
    • Subscriptions (apiome-db V258 sdk_regen_subscriptions) — one per project per ecosystem, with a deliveryMode of registry, git or registry_and_git, options and active. deliveryMode has no default, because publishing to a public registry is irreversible. options is a closed vocabulary: dryRun (registry modes only, default false) rehearses every release, and an unknown option is refused, so a misspelt one can never silently publish. Listing is projects:view. Subscribing or re-enabling needs projects:edit and versions:publish, because a subscription publishes on the tenant's behalf on every later publish. Disabling or unsubscribing needs only projects:edit.
    • The trigger — the publish route queues a run (sdk_regen_runs) and one pending job per active subscription (sdk_regen_jobs) in one statement, as a background task that never fails the publish. A project with no active subscription gains no rows.
    • The worker (app.sdk_regen_worker, ticking every APIOME_SDK_REGEN_INTERVAL) calls SDK-4.1's publish() and SDK-4.2's deliver() unchanged, so an automatic release and a manual one are identical. In registry_and_git mode the pull request is pinned to the version the publish just claimed. Jobs are claimed with FOR UPDATE SKIP LOCKED, so replicas share the queue. A subscription's publishes run in order, and one subscription's failure never blocks another's.
    • Failures go to a dead letter, like webhooks. Transient failures (a registry 5xx, a GitHub outage or rate limit) retry with backoff (4 attempts: 60s/5min/30min). Permanent ones (a missing credential, no delivery target, a registry refusal) are dead-lettered at once with a message that names the fix. A dead letter fans out an sdk.regen.dead_lettered push-webhook event and is retried with POST …/sdk-regen-jobs/{jobId}/retry (versions:publish). A step that succeeded is never repeated: a retry after a publish only delivers, and the registry result is written to the job before the git step starts, so this holds even if the worker dies. A job still running past APIOME_SDK_REGEN_LEASE_SECONDS is dead-lettered (sdk-regen-worker-lost), never retried automatically, since it may already have published. A fresh claim token per attempt stops a stale worker from overwriting a job that was retried since.
    • History links publish event → jobs → artifacts → deliveries. Each run lists its jobs, and each job gives the SDK-4.1 publish run (package, version, archive digest) and the SDK-4.2 delivery run (pull request), with links to both ledgers and to the published version. Every attempt is recorded against the runs it wrote.
    • Unsubscribing stops future runs without touching past artifacts. Queued jobs for a disabled or removed subscription are cancelled, and a removed subscription's dead letters can no longer be retried. Runs, jobs, packages and pull requests already produced are kept (ON DELETE SET NULL).
    • PublishOutcome.retryable (SDK-4.1, in-process only) now carries the registry's own RegistryUploadError.retryable, mirroring SDK-4.2's DeliveryOutcome.retryable.
    • Settings: APIOME_SDK_REGEN_ENABLED (kill switch), APIOME_SDK_REGEN_INTERVAL, APIOME_SDK_REGEN_BATCH_SIZE, APIOME_SDK_REGEN_LEASE_SECONDS. No new RBAC resource and no new credential. Documented in docs/sdk_regen_on_publish.md.

1.319.0 — 2026-09-10​

Added​

  • Git delivery, PR mode (#4496, SDK-4.2) — registry publishing serves an SDK's consumers; git delivery serves its owners. A regenerated SDK now arrives as a pull request against the tenant's own repository, where their normal review and CI apply.

    # Point the project's npm SDK at a repository already registered with Apiome
    curl -sX PUT "$APIOME/v1/projects/acme/widgets/sdk-git-delivery-targets/npm" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"repositoryId": "5f0c…", "baseBranch": "main", "targetPath": "sdks/typescript"}'

    # Deliver version 1.4.2 — opens (or updates) apiome/sdk-regen-1.4.2-widgets-npm
    curl -sX POST "$APIOME/v1/projects/acme/widgets/sdk-git-delivery" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"ecosystem": "npm", "version": "1.4.2"}'
    • No new credential type. A delivery target (apiome-db V257 sdk_git_delivery_targets: one per project per ecosystem — repository, base branch, target path) names a repository registered through a linked GitHub account, and a delivery pushes with that repository's existing linked-account token. There is no token column in V257. A repository registered from a public URL, or hosted outside GitHub, is refused with the reason.
    • The SDK-4.1 package, committed. The delivery builds the same file list a publish would upload (app.sdk_publish_pipeline now exposes resolve_release_branding, resolve_release_series and build_release_distribution), carrying the version the next registry publish of the series would claim, and commits it under the target path through GitHub's Git Database API — no clone, no git binary, no token on disk.
    • A pull request with provenance. Its body carries the spec version and revision id, the generator version, a changed-files overview and the provenance table; the commit carries Apiome-Revision / Apiome-Version-Line / Apiome-Package / Apiome-Generator trailers.
    • Idempotent per (version, target, options). The branch is apiome/sdk-regen-<version>-<project>-<ecosystem> — the ticket's apiome/sdk-regen-<version> plus the project and ecosystem, so several SDKs can share a repository without overwriting each other's pull requests. Every delivery is rebuilt on the latest base branch and force-updates the branch; re-running updates the open pull request (updated), writes nothing when it already carries this SDK (unchanged) or when the base already contains it (up_to_date), and only opens one when none is open (opened).
    • Only generated files are ever removed. A .apiome/sdk-delivery.json manifest beside the SDK lists what Apiome generated; the next delivery removes files that list named and the new one does not, and never touches anything else under the target path.
    • Failures are runs with actionable logs. Every attempt is a row in sdk_git_delivery_runs; a missing or revoked credential, a read-only token, a rejected push, a refused pull request or a package that cannot be built is a failed run with a stable errorCode, a message naming the fix, and a step log redacted of the repository token. POST …/sdk-git-delivery answers 200 with the run either way.
    • Shared run log. SDK-4.1's redacting event log moved to app.sdk_run_log.RunLog (with a per-pipeline redaction marker) so both release pipelines redact through one implementation.
    • Listing targets is projects:view; saving one is projects:edit and imports:edit (a target decides what Apiome pushes into a repository with that repository's credential); delivering is versions:publish; history is versions:view. No new RBAC resource. Documented in docs/sdk_git_delivery.md.

1.318.0 — 2026-09-09​

Added​

  • Package publishing pipelines (#4495, SDK-4.1) — downloading a zip is not how SDKs are consumed at scale, so a published version can now be released as a package a consumer installs: npm install @acme/widgets-sdk, pip install acme-widgets.

    # Store the workspace's token once (write-only; never returned by any route)
    curl -sX PUT "$APIOME/v1/tenants/acme/governance/sdk-registry-credentials/npm" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"token": "npm_..."}'

    # Validate a release without publishing anything (the default)
    curl -sX POST "$APIOME/v1/projects/acme/widgets/sdk-publish" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"ecosystem": "npm", "version": "1.4.2"}'
    • Encrypted tenant registry credentials. npm / PyPI tokens are stored in apiome-db V256 sdk_registry_credentials as ciphertext only, sealed with AES-256-GCM envelope encryption under APIOME_SDK_REGISTRY_CREDENTIAL_ENCRYPTION_KEYS. The scheme MCAT-6.2 already used was extracted to app.envelope_crypto rather than copied, and the SDK vault seals under its own magic so a blob cannot be moved between the two even on a shared key. A credential is write-only — every read returns the ecosystem, the registry, the token's public scheme prefix, its length and a truncated digest, and no route returns a token. A project credential replaces the workspace one whole (unlike SDK-3.4's settings, a token is atomic), and both scopes are managed at …/sdk-registry-credentials.
    • Publish with a dry run. POST /v1/projects/{t}/{project}/sdk-publish defaults to dryRun: true: it resolves the branding, resolves and decrypts the credential, computes the version the next real publish would claim, builds the exact archive that publish would upload and reports its SHA-256 — and uploads nothing. Because the build is byte-deterministic, the digest a dry run reports is the digest a publish uploads. Sending dryRun: false is the deliberate act; a default that published would make a mis-typed request a public release.
    • Semver from the version line and a regen counter. The published version is major.minor.<counter>, where major.minor come from the revision's version line and the counter is how many releases that line's release series has already had: 1.4 → 1.4.0, 1.4.1, 1.4.2…; v3 → 3.0.0; 2026-01-04 → 2026.1.0. The counter is allocated per series rather than per line, so 1.4.2 and 1.4.3 cannot both claim 1.4.0. A prerelease line stays a prerelease (npm 1.5.0-beta.2, PyPI 1.5.0b2), and a line with no leading number (latest, draft) is refused rather than given an invented ordering. The counter is derived from the run ledger, never stored on a project row, and a partial unique index makes that safe under concurrency — a run is inserted in_progress holding its version before the upload, and a publish that loses the race takes the next number.
    • Provenance in the package's own metadata. package.json carries an apiome object and PyPI's core metadata carries Project-URL entries naming the revision id (not merely the label — a label can be re-published, a revision cannot), the version line, the release series, the regen counter, the renderer and the SDK-3.4 settings fingerprint. An installed package therefore traces back to its spec with nothing but the package manager.
    • What is in the package. SDK-4.1's stated dependencies — the two MVP client generators (#4485/#4486), the generator SPI (#4482) and the SDK-1.1 job service and artifact store (#4481) this was to be a step on — were all closed not-planned, so, following the SDK-2.3/2.4/2.5/3.3/3.4 precedent, the pipeline publishes what did ship, read straight from the persisted canonical model: the published contract verbatim, a README, one runnable snippet per operation, and an importable module exposing the provenance and the spec. A distribution is assembled from a list of files plus metadata, so a future client generator becomes another contributor to that list rather than a second pipeline. gomod is deliberately not publishable — it names a module path, and a Go module is released by pushing a tag (SDK-4.2).
    • Secrets never reach a log. The token is read into memory, handed to the transport and never touched again; every run-log line — including anything the registry said — passes through exact-substring redaction over the token in play, so even a registry that quotes the credential it rejected cannot put one in the ledger. Uploads go through the SSRF-guarded client, so a tenant cannot point a registry URL at an internal address and be handed a token.
    • History. GET …/sdk-publish-runs[/{run_id}] lists every attempt, dry runs included, with the version it claimed, the digest it uploaded and its redacted event log.
    • Credential management is projects:view / projects:edit; publishing — including a dry run — is versions:publish. No new RBAC resource, for the reason CTG-4.4, CTG-4.5 and SDK-3.4 all gave. Documented in docs/sdk_package_publishing.md.

1.316.0 — 2026-09-08​

Added​

  • Go client generator (#4488, SDK-2.4) — Go was the most-requested third language for infra buyers, and the client kit now carries one: a complete, dependency-free Go module generated from the published contract, sitting in the kit's go/ directory beside the snippets.

    curl -sO -J "$APIOME/v1/browse/tenants/$TENANT/projects/$PROJECT/versions/1.0.0/sdk/download"
    unzip -q petstore-1.0.0-sdk.zip && cd go && go build ./... && go vet ./...
    • What it generates. go.mod (standard library only — a generated SDK that pulls in a dependency tree is a liability), a net/http transport behind an injectable Doer with WithHTTPClient / WithBaseURL / WithUserAgent / WithHeader / WithRequestEditor, exported types for every schema the contract declares, one context.Context-first method per HTTP operation, auth options derived from the model's security schemes, a typed error per declared error response, a README.md, and a runnable examples/<group>/main.go per operation group. The tenant's SDK-3.4 licence header is commented onto every .go file and its user-agent is baked in as DefaultUserAgent.
    • Why it generates from the canonical model. SDK-2.4's stated dependencies — the generator SPI (#4482) and the codegen preprocessing pass (#4483) — were both closed not-planned, as were the artifact store (#4481), the two MVP language generators (#4485/#4486) and the dashboard/CLI surfaces (#4491/#4492). Following the precedent SDK-2.3/3.3/3.4 set, app.go_client_generator is a pure function of the persisted canonical model and its output rides the existing client kit rather than an artifact store — so the download stays byte-deterministic and keeps its content-addressed ETag.
    • gomod, a third SDK-3.4 package ecosystem. A tenant configures the go.mod module path their consumers go get, with the same tenant → project merge and {tenant}/{project} tokens as the npm and PyPI patterns. Unconfigured, it falls back to example.com/<tenant>/<project>-go — example.com is IANA-reserved, so a default module path can never point at somebody's real repository.
    • It never takes a download down. An operation with no HTTP binding gets no method and is recorded with a reason, exactly as the snippets record theirs; a generator failure degrades the kit to its snippets and says so in the README and the manifest.
    • The manifest gained a go_client block (module path, package name, Go version, method and type counts, auth options, example groups, per-method coordinates and skips), and the public SDK info route gained a go_client object so the browse panel can name the module without paying to generate it.
    • The compile gate is real. tests/test_go_client_generator.py runs go build ./..., go vet ./..., gofmt -l and a go test that drives the generated client against a live httptest server — path, query, cookie, auth header, user-agent, JSON decoding and the typed 404 over an actual HTTP round trip — whenever a Go toolchain is on PATH, and skips otherwise.

Changed​

  • Reading a model's auth moved to app.canonical_security (#4488). The canonical model has no first-class security field, so importers record auth in two extras shapes that mean different things: operation.extras["security"] is a per-operation requirement, while api.extras["inferred_auth_schemes"] is only an observation that the API was seen using a scheme. Three emitters now need that distinction, so AUTH_SCHEME_HEADERS and both readers live in one module; app.http_file_emitter and app.llm_tools_emitter delegate to it. Behaviour is unchanged.
  • Three app.snippet_render helpers became public — upper_snake_token, request_message and pick_content_type — so the Go generator asks the same questions the snippets do rather than keeping a second copy of the answers. license_comment_block learned the go comment prefix.

1.315.0 — 2026-09-08​

Added​

  • Public "Get SDK" surface (#4493, SDK-3.3) — the browse portal is where API consumers land, and until now a published spec page offered them no path to a working client. It now offers two, both behind one per-project switch that is off by default.

    # Opt a project in, then take the kit.
    curl -sX PUT -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
    "$APIOME/v1/projects/$TENANT/$PROJECT/sdk-settings" \
    -d '{"settings":{"publicSdkEnabled":true}}' > /dev/null
    curl -sO -J "$APIOME/v1/browse/tenants/$TENANT/projects/$PROJECT/versions/1.0.0/sdk/download"
    • Two anonymous routes: GET /v1/browse/tenants/{t}/projects/{p}/versions/{v}/sdk describes what is on offer (the resolved package names and their install commands, the languages, the operation counts, the settings fingerprint), and …/sdk/download serves the archive itself as application/zip. Both share the MFX-7.3 public-export rate limit; the download reuses its size cap.
    • What the download is. The original scope served a generated client library from the SDK-1.1 artifact store. SDK-1.1 (#4481), the generator SPI (#4482), both language generators (#4485/#4486) and the dashboard/CLI surfaces (#4491/#4492) were closed not-planned, so there is no artifact to serve and no generator to make one. The download is instead an sdk.client-kit.v1 archive built from what did ship — the SDK-2.3 renderer — carrying a README, the published contract verbatim, and one runnable snippet per operation in ts/python/curl. It is built per request, so there is no artifact lifecycle to retain or expire.
    • publicSdkEnabled, a new key on the SDK-3.4 sdk.generation-settings.v1 body. It merges tenant → project key by key like every other setting, so a workspace can open every project at once and one project can override either way. It is the only setting that is an access control rather than branding, so it defaults to false rather than null — for a permission, "not configured" and "not allowed" are the same answer — and an unreadable settings row also reads as false: the gate fails closed. Only a real boolean is accepted; 1 is refused.
    • A project that has not opted in gets a 404, identical to the one an unpublished, private or unknown version gets. A 403 would confirm that the project exists and merely declined.
    • Provenance and determinism. The archive's manifest.json records the version record id, version label, source format, renderer, API version and merged settings fingerprint, and digests every entry. Building twice from the same inputs yields byte-identical archives, which is what lets an unstored download carry a strong content-addressed ETag (with If-None-Match → 304) and a Digest over its exact bytes.
    • Operations with no HTTP binding (gRPC, GraphQL, events) are recorded in the manifest's skipped list rather than failing the kit, and the work is capped at 250 renderable operations with truncated reported.

Changed​

  • The anonymous snippet route is now gated (#4493). GET /v1/browse/tenants/{t}/projects/{p}/versions/{v}/snippets/{operation_id} is part of the same consumer-facing SDK surface as the Get SDK download, so it now answers 404 for a project whose publicSdkEnabled is not set. Since the setting defaults to off, public snippet URLs that worked in 1.314.0 return 404 until a workspace or project owner opts in. The authenticated snippet route is unchanged — it is tenant-scoped, not public exposure.
  • Every settings fingerprint changed value. The canonical settings body carries every key, including unset ones, so that a fingerprint keeps meaning the same thing across releases; adding publicSdkEnabled therefore changes the digest the same settings produce. Stored row fingerprints are untouched until their row is next saved; the ones responses report changed immediately.
  • ExportSource now carries the revision's captured source_text and source_format, so a bundle can ship the contract alongside what was derived from it. Additive; existing callers are unaffected.
  • The deterministic zip-entry writer moved from app.export_job_engine to a shared app.zip_bundle, so the export bundle and the client kit cannot drift apart on the pinned timestamp their reproducibility depends on.

1.314.0 — 2026-09-07​

Added​

  • SDK generation settings & branding (#4494, SDK-3.4) — an organisation wants the code Apiome hands its consumers to carry the organisation's identity: packages under its own npm scope / PyPI naming pattern, its licence header on the source, its own user-agent on the traffic those clients generate. None of that is a property of any version, project row or generated file, so it now has one durable home and one set of rules.

    curl -sX PUT -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
    "$APIOME/v1/tenants/$TENANT/governance/sdk-generation-settings" \
    -d '{"settings":{"packageNamePatterns":{"npm":"@acme/{project}-sdk"},
    "userAgent":"acme-sdk/{version}"}}' | jq -r .resolved.packageNames
    • Six endpoints, three per scope: GET|PUT|DELETE /v1/tenants/{t}/governance/sdk-generation-settings and GET|PUT|DELETE /v1/projects/{t}/{project}/sdk-settings. A GET never materialises a row — a scope with nothing saved answers source: "default" — and a DELETE returns the settings now in force rather than a bare 204.
    • The merge is per key, not per row. A project that overrides only its user-agent still inherits its tenant's package pattern, and packageNamePatterns merges one ecosystem at a time. This is the one place SDK-3.4 departs from CTG-4.5's whole-body override, and the reason a stored body carries only the keys its author named: an absent key inherits the next scope up, an explicit null is deliberately none and blocks that inheritance.
    • A pattern is validated by being resolved. @acme/{project}-sdk is not itself a legal npm name, so patterns are checked by substituting probe values and validating the result. At read time a package pattern whose tokens the scope cannot fill is omitted rather than resolved approximately — a package name is an exact identifier, and a nearly-right one is worse than none. Tokens: {tenant}, {project}, {version}, {year}.
    • The snippet service applies them. SDK-2.3's authenticated and anonymous surfaces now stamp the tenant's user-agent onto the example request and its licence header above the code (as line comments — a block comment could be terminated from inside by a licence containing */), and report both back in a new branding field alongside the resolved package names. Rendering with no branding is byte-identical to before, which is what keeps FMT-2.4's bulk request-file emitter — which shares synthesize_request and render_curl — unchanged.
    • projects:view to read, projects:edit to change: no new RBAC resource and no new API-key scope. Both writes are audited as governance.sdk_generation_settings.update / .clear, with the licence header recorded by length rather than verbatim.
    • Schema: apiome-db V255 (sdk_generation_settings, two partial unique indexes, one JSONB body). Docs: apiome-rest/docs/sdk_generation_settings.md.

1.313.0 — 2026-09-07​

Added​

  • Deploy-gating status API (#4502, CTG-4.5) — a CD pipeline asks one question, can I promote this API version?, and the answer lived in five places: the GOV lint grade on the revision, CTG-3.1's breaking classification, CTG-4.2's per-consumer verdicts, CTG-4.3's conformance report and CTG-4.4's freshness. Every team that wanted a gate scripted its own multi-call version, and every one of them decided the combination rules differently.

    curl -sH "X-API-Key: $KEY" "$APIOME/v1/projects/$TENANT/$PROJECT/gate" | jq -r .status

    GET /v1/projects/{tenant_slug}/{project_ref}/gate returns pass / warn / fail plus a per-signal breakdown, each signal carrying a stable reason code, a human sentence, a link to its evidence, and the facts the judgment was made from. No analysis was added: every signal is a read of something an earlier ticket already stored — the gate never re-lints (#5259's stored reports) and never re-diffs (the stored ctg.changelog.v1 payload is rehydrated into the classified diff CTG-4.2's consumer_impact_for_diff already takes, because a changelog entry and a classified change carry the same fields).

    Partial inputs are the normal case, so there are five per-signal statuses rather than three. A signal with nothing behind it reports not_configured; one that exists but could not be read reports unknown. Both are excluded from the verdict — the endpoint has to ship before all four sources exist everywhere — and both are counted apart, because "nobody has registered a consumer", "I could not read the registry" and "no consumer is broken" are three different facts. evaluatedSignals is how a strict pipeline tells an empty gate from a real pass.

    The status is always 200. A 409 on a failing build would make a network fault and a breaking change look identical to a pipeline's error handling; the verdict is in the body and the caller owns the exit code, exactly as the CLX-4.2 lint gate does.

    Thresholds are two-rung and tenant- or project-scoped. Each signal carries a warn threshold and a fail threshold, either of which may be null to disable that rung, which is what lets one vocabulary come out of four very different measurements without a per-signal "action" dial: warn below B, fail below D says the whole lint policy in five words. A rung that could never fire is refused with a 422 listing every problem at once. GET|PUT|DELETE /v1/tenants/{tenant}/governance/deploy-gate-policy and the per-project …/gate/policy configure it, resolving project → tenant → documented default, with policy.source on every response saying which one applied. Unlike the other policy surfaces these rows are mutable rather than append-only — the gate stores no verdict, so there is no past judgment for a version history to explain; attribution lives in access_audit.

    Unlike every other policy default in the platform, this one has teeth: it fails on a breaking change and on a broken consumer, and warns on a lint grade below B or a verification older than a day. The advisory defaults elsewhere exist because those gates hang off flows that already existed and a blocking default would break people who never asked for one; nothing hangs off this one.

    Every signal fails soft, on its own. A store that is unreachable or a payload this release cannot parse costs that signal — unknown, reason signal-unavailable — and nothing else. A gate that returned 500 because one of four inputs was briefly unavailable would stop every pipeline in the tenant. The policy read degrades the same way, flagging policy.degraded so "nothing is configured" stays distinguishable from "I could not read what is".

    A schedule is matched to the gated revision however its reference was spelled (…/1.2.0, …/<uuid>, …/latest); across several, freshness is the most recent clean run and the status is the worst, so a failing staging check is never averaged away by a passing production one. When nothing schedules the version, the signal falls back to a manual CTG-4.3 report, read by resolved artifact coordinates — and a failing report never becomes the freshness anchor.

    No new RBAC resource and no new API-key scope. Reading a gate is versions:view (what a CI runner resolves to); moving the bar is verification_targets:edit, the same class of decision V211 already keeps out of an Editor's hands. The consumer signal is gated inside the response on consumer_contracts:view, reported unknown rather than leaked or silently passed. The gate is allowlisted for either CTG-2.3 CI read scope (diff:read or lint:read), since it aggregates inputs both of those already grant.

    apiome-db V254 adds deploy_gate_policy (one row per scope, two partial unique indexes) plus the artifact index the report fallback reads. See docs/deploy_gate.md.

1.311.0 — 2026-09-07​

Added​

  • Consumer-aware breaking analysis (#4480, CTG-4.2) — "breaking" is a claim about somebody. A whole-spec verdict ("47 breaking changes") forces a provider to treat every change as an incident and tells a consumer nothing. POST /v1/diff/{tenant}/classified with consumers: true now intersects the CTG-1.1 classified changes with what each registered consumer declared it uses (CTG-4.1) and answers the question that actually gates a release:

    breaks 2 of 7 consumers: billing-service, mobile-app

    Nothing was added to the database. A verdict is derived from a stored contract and a diff, and both already existed; what was missing was the join. app.consumer_impact is pure (pointer algebra, attribution rules, markdown), app.consumer_impact_service is the one place the registry is read for an analysis, and CTG-4.5's deploy gate calls the same seam.

    The exclusion is the point. A change inside a body schema, on an operation whose consumer declared which fields it reads, attributes only when one of those fields is met — removing a field nobody reads breaks nobody. Two exceptions keep that from under-reporting: an operation declared with no fields is operation-wide throughout (nothing finer was declared, so nothing finer is claimed), and a /parameters/… change is always operation-wide, because a newly required query parameter breaks every caller whether or not they declared it. Root security, servers and securitySchemes changes reach every declared consumer.

    Pointer overlap is segment-aware, unlike the starts_with narrowing query behind db.find_consumer_contracts_by_pointers: /components/schemas/Pet no longer "touches" /components/schemas/PetFood. The SQL only narrows the candidate set; this renders the verdict.

    The denominator is honest. A consumer registered without a current contract cannot be counted as safe, so it is reported beside the fraction (2 registered consumers have declared no surface) with verdict undeclared, never inside it. Each verdict also carries contractMatchesBase, so a surface resolved against a different revision is flagged rather than silently equated.

    Changes touching nobody stay globally classified and are flagged: each change gains a consumers array ([] = no registered consumer affected, null = not analysed), and the report's attribution list is exact even when a consumer's impacts enumeration is capped.

    Markdown responses gain a Consumer impact section, and apiome diff --consumers prints breaks billing-service lines in text, JSON and markdown. The CI exit code is unchanged — --fail-on still grades the whole specification, so a build never passes merely because nobody has registered as a consumer yet.

    See apiome-rest/docs/consumer_impact.md.

1.310.0 — 2026-09-07​

Added​

  • Provider verification against a live deployment (#4489, CTG-4.3) — a published specification can be perfectly versioned and still lie. POST /v1/tenants/{tenant}/contracts/{version_ref}/ verify-provider executes the version's compiled contract suite against a registered deployment and returns a conformance report: per-operation verdicts, every schema violation located by its JSON Pointer into the response body, and coverage measured against every operation the specification declares.

    Nothing here re-implements execution. ECA-1.1 compiles the requests, ECA-1.2 holds the target and its credential reference, ECA-2.1 sends them and validates responses, ECA-1.3 stores the run. What was missing was the judgment: coverage had no denominator (a run recorded the cases it executed, never the operations it did not reach), drift had no location (the validator's JSON Pointers were computed and then discarded), mutation was an all-or-nothing target flag, and there was no report object a deploy gate could fetch.

    Safe by default. Only GET/HEAD/OPTIONS are sent. A mutating case runs only when the target policy permits mutation, and the run opts in, and a tenant-supplied fixture names the operation — because "you may write" and "here is the row you may write to" are different permissions. A run can narrow what the registry permits and never widen it; a fixture supplies the request but never the expectation, so it cannot make a failing case pass; a fixture carrying a credential header is refused; and a fixture never overwrites a negative case's body or restores the parameter that case exists to omit. A fixture that matches nothing is reported, not dropped.

    Coverage is honest. The denominator counts operations the suite compiler could not compile, so a coverage number cannot be inflated by a document half of which was skipped, and every operation the report does not vouch for is named with its reason.

    Reports are persisted write-once beside their evidence (apiome-db V252, provider_verification_report) with the coverage numbers and verdict as indexed columns, so CTG-4.4 (scheduled verification) and CTG-4.5 (deploy gating) can ask "is the newest report for this version passing?" without parsing a report body. No new RBAC resource: a conformance report is verification evidence. See docs/provider_verification.md.

Changed​

  • app/contract_runner.py (ECA-2.1) now records one assertion per located schema violation alongside the existing headline verdict, with the instance JSON Pointer as the assertion subject, and accepts an optional per-case decisions map so a caller can hold a case back or substitute a request without reimplementing the runner. Both are additive: omitting decisions is exactly the previous behaviour, and the headline assertion's shape is unchanged.

1.308.0 — 2026-08-31​

Changed​

  • There is now one mock engine (#5532, MSC-2.2) — the mock implementation that lived inside apiome-rest is deleted, and every mock request is resolved by apiome-mock.

    There were two. This one served /v1/mock/{id}/… — the short-lived sandbox instances the hosted Mock Server (#3615) and the Export Studio's test drive (MFX-44.5) provision — from mock_instances.config, a list of scenarios with rules. apiome-mock served /{tenant}/{project}/{version}/… and portable bundles from versions.mock_settings, a dict keyed by scenario name. They shared three routing symbols and reimplemented everything downstream, so a feature built in one was invisible in the other: sandboxes had no templates, no match predicates, no stateful CRUD, no fixture packs and no chaos, and every future mock feature would have had to be written twice.

    A sandbox request now takes one internal hop. apiome-rest keeps what a sandbox is — does the instance exist, has it expired, is the caller inside its rate limit, what goes in the request log — builds the instance's portable mock bundle, and asks apiome-mock's new /__sandbox__ endpoint to serve it through serve_compiled_request, the same function the hosted data plane and the portable runtime call. Sandboxes gain every feature they lacked as a side effect.

    There is deliberately no local fallback: a deployment that has not configured APIOME_MOCK_INTERNAL_BASE_URL / APIOME_MOCK_INTERNAL_TOKEN answers 503 on the data plane and says which switch is missing, rather than inventing an answer from a second engine.

  • The built-in scenarios still resolve, everywhere (#5532) — happy-path, server-error, not-found and slow are written into client code by name, so all four are now defined on every version, supplied by the runtime and merged into whatever the version stores. A stored scenario of the same name wins outright, exactly as the retired engine's normalize_scenarios resolved the same collision.

Added​

  • Two additions to the scenario schema (#5532) — both needed to express the built-ins and the migrated rules, and both available to any authored scenario:

    • the operation key "*", which applies to every operation a scenario does not name explicitly (an exact key always wins). Its canned bodies are not checked against the spec at save time, because a wildcard covers operations with different response schemas; status, media type, headers and template syntax still are;
    • an override's status, which pins the response status but leaves the body to the spec, resolved as a request sending ?__status= resolves it. A canned response with no body would serve an empty one, and freezing a synthesized body into settings would stop it tracking the spec.
  • migrationNotes on a mock instance (#5532) — every rule in a pre-fold config that could not be translated is reported on the instance and stored in mock_instances.migration_notes: one that matched no operation in the frozen spec, one an earlier rule had already made unreachable, one that set nothing, or one whose latency was clamped to the 30 s ceiling. The acceptance criterion is that untranslatable rules are reported, never silently dropped.

Migration​

  • V250__mock_instance_engine_fold_5532.sql adds mock_instances.settings (the apiome-mock-shaped configuration an instance is served from) and migration_notes. settings is NULL until an instance is folded; either plane folds a row the first time it reads one, so no maintenance window is required. apiome-rest/scripts/fold_mock_instance_configs.py folds the whole estate at once and prints the report (--dry-run translates without writing).

    The translation (app.mock_instance_config) is spec-aware, because three legacy behaviours cannot be reproduced from the stored shapes alone: a body-only rule served the operation's own default success status; precedence was first-matching-rule-wins per operation, which inverts in the keyed shape where an exact key beats the wildcard; and a rule matching nothing in the frozen spec did nothing. active_scenario lands on activeScenario, the key #5531 introduced — the two spellings of one concept are now one. The legacy config column is kept, unread, as the pre-fold record a migrated instance is diffed against.

Removed​

  • app.mock_engine and app.mock_data_generator (#5532) — the retired engine's resolver (resolve_response, normalize_scenarios, resolve_active_scenario_name, BUILTIN_SCENARIOS) and its data generator, superseded by apiome_mock's resolver and its format-aware schema_synthesizer. What both packages genuinely shared — MockOperation, extract_operations, match_operation and the path-template compiler — moved to app.mock_routing, whose name says that it is routing and not an engine. The one piece of mock_data_generator with no runtime counterpart, validate_value, moved to app.mock_schema_validation.

1.307.0 — 2026-08-31​

Added​

  • The version's active scenario is now a mock setting the hosted runtime honours (#5531, MSC-2.1) — GET/PUT .../mock/scenarios carry activeScenario, the scenario the mock serves when a request sends no X-Mock-Scenario header. It is stored in versions.mock_settings alongside the scenarios it names, travels inside a portable mock bundle, and is a section of the MSC-1.4 configuration document.

    Until now the control plane could store and switch an active scenario that nothing in the hosted data plane read: grep -rn "active_scenario" apiome-mock/src returned nothing, and the runtime selected a scenario from the request header alone. Switching a mock to server-error and then calling the hosted URL returned happy-path responses, with nothing to indicate why. A shipped control that changes nothing is a correctness problem, not a nicety.

    The precedence is explicit: request header → stored activeScenario → no scenario. The header stays an outright override, and a version with no stored value behaves byte-identically to before. Every response served while a scenario is in effect now names it in the X-Mock-Scenario response header — including responses for operations the scenario does not override, because a caller who sent no header cannot otherwise tell what answered them.

    activeScenario must name one of the scenarios saved with it, so a version can never store a default that means nothing; the runtime, by contrast, is lenient with a stored name that stops resolving (a renamed or deleted scenario) — it logs mock_active_scenario_unknown and serves the default flow, because an unresolvable default must never take a serving mock down.

    Unlike scenarios and chaos, the field is preserved when a PUT omits it and cleared only when it is sent as null, so an editor written before the field existed cannot silently switch a version's mock back to its default flow.

Changed​

  • Mock bundles carry activeScenario (#5531) — a bundle exported from a version defaults to the same scenario the hosted mock does, asserted response-by-response by the PMR-3.1 parity harness.
  • The MSC-1.2 preview trace reports scenarioSource — "header" or "config" — so a preview says why a scenario applied, not merely that one did.

1.306.0 — 2026-08-29​

Added​

  • ?dryRun=true on the version mock-settings write routes (#5530, MSC-1.4) — PUT .../mock/scenarios, PUT .../mock/correlation and PUT .../mock/fixture-packs accept ?dryRun=true: the same validation runs, the response describes what would be stored, and nothing is written.

    It exists so that a mock configuration held as a file — apiome mock config push --dry-run — can be checked in CI against the authoritative validators rather than a second copy of them, and so that a real push can validate every section before any of them is stored. Without it a document whose scenarios are valid and whose correlation block is not would leave a version half-configured, and would report only the first problem rather than all of them.

    The dry-run response is built by the very readers the write path reports with, so it cannot describe an outcome the write would not produce — operation keys normalize the same way, fixture packs gain the same format defaults, and a cleared correlation block reports as null. It reports validation, not ownership: the creator/administrator gate lives in the write itself and cannot be exercised without writing.

    The parameter defaults to off, so every existing caller is unaffected.

1.305.0 — 2026-08-28​

Added​

  • Mock authoring catalogue endpoint (#5529, MSC-1.3) — GET /v1/versions/{tenant_slug}/{project_id}/{version_record_id}/mock/operations describes a version the way a mock editor needs it: every operation it has, with its path, query and header parameters (each carrying the ready-to-insert {{request.*}} expression), the JSON Pointers the success response body actually has, the top-level request-body field names, and the fixture names {{fixture.<name>}} can read on this version.

    The part that makes response correlation trustworthy is bindings: which response properties the path-params and inferred passes would bind, and to what, projected before anything is saved. Those are not a second implementation of inference — the name-matching rules moved into the new app.mock_correlation_rules, which apiome_mock.correlation now imports, the same call app.mock_match and app.mock_template already made. So the editor's preview cannot promise a binding the runtime declines to make.

    Two limits follow from projecting over a response schema rather than a rendered body, and are reported rather than hidden: a pointer inside an array names member 0 and is flagged repeated (the runtime binds every member), and a oneOf/anyOf schema is projected through its first branch. The walk is bounded — depth 6, 200 pointers per operation, and a $ref cycle guard — so a recursive schema terminates.

    Read-only and cheap: it generates the version's OpenAPI document and walks it, needs only versions:view, writes nothing, and answers for a version whose mock is switched off — which is when correlation is usually configured for the first time.

1.304.0 — 2026-08-28​

Added​

  • Mock response preview endpoint (#5528, MSC-1.2) — POST /v1/versions/{tenant_slug}/{project_id}/{version_record_id}/mock/preview answers the question a mock author actually has: given this request, what does the mock return? Send a synthetic request (method, path, headers, query, body, plus scenario/seed shorthands) and get back the status, headers, media type and body the mock would serve — with no mock enabled, no instance provisioned, and no live request.

    Every response carries a decision trace naming which layer produced the body: scenario (with the matched rule's zero-based index), stateful, correlation (with the mode and the JSON Pointers it bound), example or synthesis — plus forced-status, request-invalid, no-operation, method-not-allowed, unknown-scenario, not-acceptable and template-limit for the paths that produce no ordinary body. Without it an author can see that a value appeared but not why, which is most of the value of a preview.

    The render is not re-implemented here. REST authenticates and authorizes the caller, builds the version's portable mock bundle, and asks apiome-mock's internal /__preview__ endpoint to render it through serve_compiled_request — the same function its data plane and the portable runtime call. A preview therefore cannot disagree with the served response, and apiome-mock's suite asserts exactly that by rendering one correlated request both ways.

    An optional settings override previews an unsaved draft: it overlays the stored settings per key (scenarios, chaos, fixture packs, callbacks, correlation), is canonicalized and validated with the same rules its save route applies — so a draft that could never be saved is a 422 rather than a silently ignored no-op — and persists nothing. It requires versions:edit; previewing the stored settings needs only versions:view.

    Preview never writes: session state lives and dies inside the render, no callback is delivered, and no usage, audit or provisioning row is touched. Chaos is reported rather than applied, so a preview never sleeps for a configured latency or randomly answers 500. Renders are rate limited per version (APIOME_MOCK_PREVIEW_RATE_LIMIT_PER_MINUTE, default 120) and the synthetic body is capped (APIOME_MOCK_PREVIEW_MAX_BODY_BYTES, default 256 KiB).

    Configuration: APIOME_MOCK_INTERNAL_BASE_URL (the mock service's internal address) and APIOME_MOCK_INTERNAL_TOKEN (shared with apiome-mock). Both are required — with either unset the endpoint fails closed with 503. The token is deliberately not INTERNAL_SERVICE_TOKEN: rendering a preview should not carry the secret that unseals auth-provider credentials. See docs/guide/mock-response-preview.md.

1.303.0 — 2026-08-28​

Added​

  • Request-correlated mock responses (#5527, MSC-1.1) — GET/PUT /v1/versions/{tenant_slug}/{project_id}/{version_record_id}/mock/correlation read and write a responseCorrelation block in versions.mock_settings, alongside the existing scenarios, chaos, fixturePacks and callbacks keys. The block makes the mock's default response path answer with the request's own values — with no request header, so a generated SDK or a browser app gets it too, where scenario overrides (X-Mock-Scenario) and stateful CRUD (X-Mock-Session) could never reach.

    Four modes: off (the default — byte-identical to today), path-params (a response property named after a path parameter takes the request's value, at every depth and inside array members), inferred (that plus echoing request-body fields back on POST/PUT/PATCH, while id/createdAt/updatedAt and anything absent from the request stay synthesized), and explicit (only a per-operation map of response JSON Pointer to template expression). The pointer map applies in every mode except off and always wins for the pointer it names.

    Expressions are the existing bounded {{ … }} language, validated on save with validate_template_value, so a bad expression is a 422 rather than a serve-time surprise; operation keys must name a real operation in the version's generated OpenAPI document; bindings saved with mode: "off" are refused rather than silently ignored. Limits: 200 operations, 50 pointers each, 64 KiB. responseCorrelation is a bundled settings key, so a portable bundle correlates identically offline (the PMR-3.1 parity harness asserts it). See docs/guide/mock-response-correlation.md.

Removed​

  • The dead mock_settings.fixtures reader (#5527) — apiome_mock.fixture_data.parse_fixtures read a flat fixtures map that nothing in apiome-rest ever wrote; only fixture packs are wired. A configuration surface that silently did nothing was dropped rather than given a writer. Hosted template fixture data comes from fixturePacks alone; the bundle path is unchanged.