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.pyrendersopenapi.yamlintoapiome-docs/docs/reference/rest/— one page per tag (plusuntagged) with every operation's parameters, request body, responses and the schemas it uses. The index records the document's SHA-256 (openapi_sha256), andyarn docs:checkfails whenopenapi.yamlchanges without regenerating;--checkandtests/test_rest_reference_docs.pyfail on stale pages. Rendering lives inapp.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
docsPagethe API returns now names a page of the Docusaurus site instead of the retireddocs/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-uiresolves it tohttps://apiome.github.io/apiome/build/lint-rules. Affected fields: lint-rule catalog and style-guidedocsPage, blocking-ruledocs_page(schema →build/lint-rules, MCP →govern/mcp-*-rules), axisalgorithmDocsPage(build/axis-score) and import-preflight rule links. Blocking schema-rulereferenceURLs now point at the site (https://apiome.github.io/apiome/build/lint-rules#<rule>).- New
app.docs_sitebuilds those paths, the site URLs and the pages' front matter. scripts/generate_lint_rule_docs.py,generate_supported_formats_doc.pyandgenerate_format_counts.pywrite intoapiome-docs/docs/**; generated pages open with front matter and mark rule / format anchors as heading ids (###rule.id{#rule-id}).- OpenAPI
info.version1.204.0 (description text only).
- New
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) orreject. Only accepted text is compiled, and only while the toolset's newdescriptionEnrichmentsetting is on (the default). V273 addsagent_toolset_enrichments; its CHECK makes accepted text without a review unrepresentable. GET …/compiledreturns 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.
- Agent-hostile flags with machine-readable reasons:
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, withenabledandtarget(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/HEADoperations and GraphQL queries (deprecated ones excepted). Every other operation is a write op and starts disabled. Enabling one withoutconfirmWriteOp: trueis a422 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, andPATCH …/{id}/tools/{toolId}. Guarded by the existingapi_keyspermissions; no new RBAC resource. - Audited.
agent.toolset.create,agent.toolset.update(before/after),agent.toolset.deleteandagent.toolset.tool.update(operation, write-op flag, enabled before/after, confirmation time), with metadata only. - Docs:
docs/agent_toolsets.md.
- Two tables (V270).
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_idcolumns a(tenant_id, toolset_id)foreign key toagent_toolsetswithON DELETE CASCADE. Deleting a toolset now deletes its upstream credentials and agent keys.POST /agent-keysand the upstream-credential list/create routes answer404 agent-toolset-not-foundfor 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_keysextended (V269).kind(workspace|agent; existing rows areworkspace),toolset_idandtool_allowlist(a JSON array of AGX-1.1 tool names, at most 1024, no wildcard).expires_atis reused. CHECKs keep the two kinds exclusive and pin agent keys to the scopeagent:invoke.- Five routes under
/v1/tenants/{t}/agent-keys: list (?toolsetId,?includeRevoked), create, get,PUT …/{id}/allowlist, andDELETE …/{id}(revoke, idempotent). Guarded by the existingapi_keysview / 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) andagent.key.revokego to the access audit, with metadata only. - Enforced in apiome-mcp.
apiome_mcp.agent_access.AgentAccessMiddlewarenarrowstools/listandtools/callto 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_idhas no foreign key yet (agent_toolsetsis AGX-1.2). Docs:docs/agent_keys.md.
Security
- An agent key never authenticates a REST call.
validate_api_keynow selectskind = 'workspace'keys only, and falls back to the older queries on a pre-V269 database. Separately, no REST scope-allowlist entry acceptsagent:invoke, so an agent key would get a403on 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 a503and nothing is written in the clear. - Write-only.
GETlists 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 everyGETroute in the app for any rendering of a stored secret or its ciphertext. The router's422s 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_usesledger (which credential, which toolset, when,injected/unavailable); create / rotate / delete land in the access audit asagent.upstream_credential.*. A bound credential that won't open fails the call closed. - Guarded by the existing
api_keyspermission (view / create / edit / delete); no new RBAC resource.toolset_idgains its foreign key in AGX-1.2 (#4530). Seedocs/upstream_credential_vault.md.
- Encrypted at rest.
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,failorskipped— recorded as an evaluation, reported on the pull request as theapiome/api-changecheck (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),breakingandconsumers(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) andsdk(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,advisoryoroff; 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 componentpendingand skips an advisory one; unreadable evidence ispending, 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, otherwisepending(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
200replay 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 withoutconsumer_contracts:view. - Required before publish.
requiredForPublishon 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 asversion.api_check_suite_gate. - Endpoints:
POST/GET …/projects/{project}/versions/{version}/check-suite,GET …/runs,GET …/projects/{project}/check-suite/runs/{run_id}, andGET/PUT/DELETEof…/governance/check-suite-policyand…/projects/{project}/check-suite-policy(tenant administrators only for changes). The latest-evaluation read is allowlisted fordiff:read/lint:readCI 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) andapiome.api_check_suite_runs(append-only except theON DELETE SET NULLof its author; a placeholder can never pass or fail), plus an index on ECA-1.3 runs bysource->>'revision_id'.
- Five components, all existing evidence.
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) andapiome.draft_sync_conflicts(one row per overlap, carrying all three values with the repository file and line it lives at, settled once towardsgitordraft). - 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 ofdatabase.py, whosesync.planned/sync.conflict_resolvedaudit 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/schemagrouping 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 ofsummary: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-forbiddenrather 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 withsync-base-driftedinstead 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) orversion_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.
- A merge result is a reading, never a write. There is no code path from this surface to the
canonical model, to
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 intostatusandconclusion, GitLab has one commitstate, Bitbucket has build states in capitals and noskippedat all — so storing any one of them would make the other two lossy translations. Onlypassever 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 — withcompleted_attied to the state by a CHECK and the identity frozen by a trigger) andapiome.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(oneStatusAdaptercontract; 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 ofdatabase.py, whosecheck.recorded/check.publishedaudit 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
failedrow 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
pendingcheck 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 carrieschecksSeeded. - 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.
- Four words, provider-independent:
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) andapiome.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 ofdatabase.py, whosebinding.*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_auditwas 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.
- apiome-db V264:
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 badgecurl -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 centrecurl -s "$APIOME/v1/tenants/acme/notifications?unread=true&limit=20" -H "Authorization: Bearer $TOKEN"# Clearing itcurl -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
notifycallable and runs it inside its own transaction, so a committed event always has its rows and a refused one leaves none. The insert joinsusers, 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(filtersunread,type, paged),GET …/notifications/unread-count(total and per type, zeroes included), andPOST …/notifications/read(idsorall, idempotent, answering with the new count). All three need only authentication — an inbox is the caller's own, which noresource:actioncan 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.
- Storage (apiome-db V263):
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 guidecurl -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 verdictcurl -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) andstyle_guides.required_reviewer_role(aroles.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 eachstyle_guide_revisionssnapshot. Read/written atGET/PUT /v1/style-guides/{tenantSlug}/{guideId}/policy, whererequiredReviewerRoleis 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
reasonforno-review,changes-requested,spec-changed(the content moved since the round was requested, so its approvals are stale — COL-2.1'sspec_changed),insufficient-approvals, andmissing-required-role. A reviewer's role is their effective RBAC slug, so a tenant administrator resolves toowner. - 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_gateworkflow_auditrow withactionofsatisfied,forced, orunavailable— passing verdicts included, so the trail can answer "who signed off on this release?". - Faults degrade to
unavailableand never block a publish; the degradation is audited rather than silent. - Documented in
docs/approval_publish_gate.md; 46 tests intests/test_approval_publish_gate.py.
- Policy (apiome-db V262):
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 labelcurl -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 versioncurl -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):
statusgainsorphaned, withanchor_label(the element's name when it was deleted —Customer,Customer.email,/customers/{id},GET /customers/{id}) andorphaned_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 backresolvedif it was,openotherwise.404 comment-anchor-not-foundfor a target outside the version,409 comment-thread-not-orphanedfor a thread that is not orphaned. Requiresprojects: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.
- Orphaning (apiome-db V260):
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 teammatecurl -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 mecurl -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) andcomments(thread_id,author_id, Markdownbody,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 (filtersversion,status,anchor_type,anchor_id,mentions_me; paged with atotal; each row carries its opening comment), open, read, delete,resolve,reopen, and…/commentsto reply, edit, and delete. Deleting a thread's last comment deletes the thread. - Permissions: no new RBAC resource.
projects:viewis 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 returns429 comment-rate-limited. Documented indocs/comments.md.
- Storage (apiome-db V259):
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 versioncurl -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 lettercurl -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 adeliveryModeofregistry,gitorregistry_and_git,optionsandactive.deliveryModehas no default, because publishing to a public registry is irreversible.optionsis a closed vocabulary:dryRun(registry modes only, defaultfalse) rehearses every release, and an unknown option is refused, so a misspelt one can never silently publish. Listing isprojects:view. Subscribing or re-enabling needsprojects:editandversions:publish, because a subscription publishes on the tenant's behalf on every later publish. Disabling or unsubscribing needs onlyprojects: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 everyAPIOME_SDK_REGEN_INTERVAL) calls SDK-4.1'spublish()and SDK-4.2'sdeliver()unchanged, so an automatic release and a manual one are identical. Inregistry_and_gitmode the pull request is pinned to the version the publish just claimed. Jobs are claimed withFOR 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_letteredpush-webhook event and is retried withPOST …/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 pastAPIOME_SDK_REGEN_LEASE_SECONDSis 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 ownRegistryUploadError.retryable, mirroring SDK-4.2'sDeliveryOutcome.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 indocs/sdk_regen_on_publish.md.
- Subscriptions (apiome-db V258
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 Apiomecurl -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-npmcurl -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_pipelinenow exposesresolve_release_branding,resolve_release_seriesandbuild_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-Generatortrailers. - Idempotent per (version, target, options). The branch is
apiome/sdk-regen-<version>-<project>-<ecosystem>— the ticket'sapiome/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.jsonmanifest 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 afailedrun with a stableerrorCode, a message naming the fix, and a step log redacted of the repository token.POST …/sdk-git-deliveryanswers200with 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 isprojects:editandimports:edit(a target decides what Apiome pushes into a repository with that repository's credential); delivering isversions:publish; history isversions:view. No new RBAC resource. Documented indocs/sdk_git_delivery.md.
- No new credential type. A delivery target (apiome-db V257
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_credentialsas ciphertext only, sealed with AES-256-GCM envelope encryption underAPIOME_SDK_REGISTRY_CREDENTIAL_ENCRYPTION_KEYS. The scheme MCAT-6.2 already used was extracted toapp.envelope_cryptorather 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-publishdefaults todryRun: 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. SendingdryRun: falseis 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>, wheremajor.minorcome 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, so1.4.2and1.4.3cannot both claim1.4.0. A prerelease line stays a prerelease (npm1.5.0-beta.2, PyPI1.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 insertedin_progressholding its version before the upload, and a publish that loses the race takes the next number. - Provenance in the package's own metadata.
package.jsoncarries anapiomeobject and PyPI's core metadata carriesProject-URLentries 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.
gomodis 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 — isversions:publish. No new RBAC resource, for the reason CTG-4.4, CTG-4.5 and SDK-3.4 all gave. Documented indocs/sdk_package_publishing.md.
- Encrypted tenant registry credentials. npm / PyPI tokens are stored in apiome-db V256
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), anet/httptransport behind an injectableDoerwithWithHTTPClient/WithBaseURL/WithUserAgent/WithHeader/WithRequestEditor, exported types for every schema the contract declares, onecontext.Context-first method per HTTP operation, auth options derived from the model's security schemes, a typed error per declared error response, aREADME.md, and a runnableexamples/<group>/main.goper operation group. The tenant's SDK-3.4 licence header is commented onto every.gofile and its user-agent is baked in asDefaultUserAgent. - 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_generatoris 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-addressedETag. gomod, a third SDK-3.4 package ecosystem. A tenant configures thego.modmodule path their consumersgo get, with the same tenant → project merge and{tenant}/{project}tokens as the npm and PyPI patterns. Unconfigured, it falls back toexample.com/<tenant>/<project>-go—example.comis 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_clientblock (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 ago_clientobject so the browse panel can name the module without paying to generate it. - The compile gate is real.
tests/test_go_client_generator.pyrunsgo build ./...,go vet ./...,gofmt -land ago testthat drives the generated client against a livehttptestserver — path, query, cookie, auth header, user-agent, JSON decoding and the typed 404 over an actual HTTP round trip — whenever a Go toolchain is onPATH, and skips otherwise.
- What it generates.
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 twoextrasshapes that mean different things:operation.extras["security"]is a per-operation requirement, whileapi.extras["inferred_auth_schemes"]is only an observation that the API was seen using a scheme. Three emitters now need that distinction, soAUTH_SCHEME_HEADERSand both readers live in one module;app.http_file_emitterandapp.llm_tools_emitterdelegate to it. Behaviour is unchanged. - Three
app.snippet_renderhelpers became public —upper_snake_token,request_messageandpick_content_type— so the Go generator asks the same questions the snippets do rather than keeping a second copy of the answers.license_comment_blocklearned thegocomment 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/nullcurl -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}/sdkdescribes what is on offer (the resolved package names and their install commands, the languages, the operation counts, the settings fingerprint), and…/sdk/downloadserves the archive itself asapplication/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.v1archive 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.4sdk.generation-settings.v1body. 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 tofalserather thannull— for a permission, "not configured" and "not allowed" are the same answer — and an unreadable settings row also reads asfalse: the gate fails closed. Only a real boolean is accepted;1is refused.- A project that has not opted in gets a 404, identical to the one an unpublished, private or
unknown version gets. A
403would confirm that the project exists and merely declined. - Provenance and determinism. The archive's
manifest.jsonrecords 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-addressedETag(withIf-None-Match→ 304) and aDigestover its exact bytes. - Operations with no HTTP binding (gRPC, GraphQL, events) are recorded in the manifest's
skippedlist rather than failing the kit, and the work is capped at 250 renderable operations withtruncatedreported.
- Two anonymous routes:
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 whosepublicSdkEnabledis 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
publicSdkEnabledtherefore changes the digest the same settings produce. Stored row fingerprints are untouched until their row is next saved; the ones responses report changed immediately. ExportSourcenow carries the revision's capturedsource_textandsource_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_engineto a sharedapp.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-settingsandGET|PUT|DELETE /v1/projects/{t}/{project}/sdk-settings. AGETnever materialises a row — a scope with nothing saved answerssource: "default"— and aDELETEreturns the settings now in force rather than a bare204. - The merge is per key, not per row. A project that overrides only its user-agent still
inherits its tenant's package pattern, and
packageNamePatternsmerges 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 explicitnullis deliberately none and blocks that inheritance. - A pattern is validated by being resolved.
@acme/{project}-sdkis 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 newbrandingfield 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 sharessynthesize_requestandrender_curl— unchanged. projects:viewto read,projects:editto change: no new RBAC resource and no new API-key scope. Both writes are audited asgovernance.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.
- Six endpoints, three per scope:
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 .statusGET /v1/projects/{tenant_slug}/{project_ref}/gatereturnspass/warn/failplus 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 storedctg.changelog.v1payload is rehydrated into the classified diff CTG-4.2'sconsumer_impact_for_diffalready 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 reportsunknown. 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.evaluatedSignalsis how a strict pipeline tells an empty gate from a real pass.The status is always 200. A
409on 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
nullto 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 a422listing every problem at once.GET|PUT|DELETE /v1/tenants/{tenant}/governance/deploy-gate-policyand the per-project…/gate/policyconfigure it, resolving project → tenant → documented default, withpolicy.sourceon 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 inaccess_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, reasonsignal-unavailable— and nothing else. A gate that returned500because one of four inputs was briefly unavailable would stop every pipeline in the tenant. The policy read degrades the same way, flaggingpolicy.degradedso "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 isverification_targets:edit, the same class of decision V211 already keeps out of an Editor's hands. The consumer signal is gated inside the response onconsumer_contracts:view, reportedunknownrather than leaked or silently passed. The gate is allowlisted for either CTG-2.3 CI read scope (diff:readorlint: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. Seedocs/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}/classifiedwithconsumers: truenow 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-appNothing 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_impactis pure (pointer algebra, attribution rules, markdown),app.consumer_impact_serviceis 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. Rootsecurity,serversandsecuritySchemeschanges reach every declared consumer.Pointer overlap is segment-aware, unlike the
starts_withnarrowing query behinddb.find_consumer_contracts_by_pointers:/components/schemas/Petno 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 verdictundeclared, never inside it. Each verdict also carriescontractMatchesBase, 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
consumersarray ([]= no registered consumer affected,null= not analysed), and the report'sattributionlist is exact even when a consumer'simpactsenumeration is capped.Markdown responses gain a Consumer impact section, and
apiome diff --consumersprintsbreaks billing-servicelines in text, JSON and markdown. The CI exit code is unchanged —--fail-onstill 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-providerexecutes 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/OPTIONSare 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. Seedocs/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-casedecisionsmap so a caller can hold a case back or substitute a request without reimplementing the runner. Both are additive: omittingdecisionsis 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 — frommock_instances.config, a list of scenarios withrules. apiome-mock served/{tenant}/{project}/{version}/…and portable bundles fromversions.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 throughserve_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_TOKENanswers503on 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-foundandsloware 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'snormalize_scenariosresolved 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.
- the operation key
-
migrationNoteson a mock instance (#5532) — every rule in a pre-foldconfigthat could not be translated is reported on the instance and stored inmock_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.sqladdsmock_instances.settings(the apiome-mock-shaped configuration an instance is served from) andmigration_notes.settingsis 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.pyfolds the whole estate at once and prints the report (--dry-runtranslates 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_scenariolands onactiveScenario, the key #5531 introduced — the two spellings of one concept are now one. The legacyconfigcolumn is kept, unread, as the pre-fold record a migrated instance is diffed against.
Removed
app.mock_engineandapp.mock_data_generator(#5532) — the retired engine's resolver (resolve_response,normalize_scenarios,resolve_active_scenario_name,BUILTIN_SCENARIOS) and its data generator, superseded byapiome_mock's resolver and its format-awareschema_synthesizer. What both packages genuinely shared —MockOperation,extract_operations,match_operationand the path-template compiler — moved toapp.mock_routing, whose name says that it is routing and not an engine. The one piece ofmock_data_generatorwith no runtime counterpart,validate_value, moved toapp.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/scenarioscarryactiveScenario, the scenario the mock serves when a request sends noX-Mock-Scenarioheader. It is stored inversions.mock_settingsalongside 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/srcreturned nothing, and the runtime selected a scenario from the request header alone. Switching a mock toserver-errorand 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 theX-Mock-Scenarioresponse header — including responses for operations the scenario does not override, because a caller who sent no header cannot otherwise tell what answered them.activeScenariomust 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 logsmock_active_scenario_unknownand serves the default flow, because an unresolvable default must never take a serving mock down.Unlike
scenariosandchaos, the field is preserved when aPUTomits it and cleared only when it is sent asnull, 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=trueon the version mock-settings write routes (#5530, MSC-1.4) —PUT .../mock/scenarios,PUT .../mock/correlationandPUT .../mock/fixture-packsaccept?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/operationsdescribes 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 thepath-paramsandinferredpasses 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 newapp.mock_correlation_rules, whichapiome_mock.correlationnow imports, the same callapp.mock_matchandapp.mock_templatealready 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
0and is flaggedrepeated(the runtime binds every member), and aoneOf/anyOfschema is projected through its first branch. The walk is bounded — depth 6, 200 pointers per operation, and a$refcycle 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/previewanswers the question a mock author actually has: given this request, what does the mock return? Send a synthetic request (method,path,headers,query,body, plusscenario/seedshorthands) 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),exampleorsynthesis— plusforced-status,request-invalid,no-operation,method-not-allowed,unknown-scenario,not-acceptableandtemplate-limitfor 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 throughserve_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
settingsoverride 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 requiresversions:edit; previewing the stored settings needs onlyversions: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) andAPIOME_MOCK_INTERNAL_TOKEN(shared with apiome-mock). Both are required — with either unset the endpoint fails closed with 503. The token is deliberately notINTERNAL_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/correlationread and write aresponseCorrelationblock inversions.mock_settings, alongside the existingscenarios,chaos,fixturePacksandcallbackskeys. 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 onPOST/PUT/PATCH, whileid/createdAt/updatedAtand anything absent from the request stay synthesized), andexplicit(only a per-operation map of response JSON Pointer to template expression). The pointer map applies in every mode exceptoffand always wins for the pointer it names.Expressions are the existing bounded
{{ … }}language, validated on save withvalidate_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 withmode: "off"are refused rather than silently ignored. Limits: 200 operations, 50 pointers each, 64 KiB.responseCorrelationis 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.fixturesreader (#5527) —apiome_mock.fixture_data.parse_fixturesread 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 fromfixturePacksalone; the bundle path is unchanged.
