Skip to main content

catalog/v1alpha1

Provider realization implemented; index service specified

Rendered from contracts/catalog/v1alpha1/semantics.md by connectors-docs; the source owns every rule on this page.

  • Status: proposed, not implemented.
  • Family: catalog. No siblings. Companion profile defined here: operations/v1alpha1 generic-http (the realization that makes a catalogued operation callable without handwritten provider code).
  • Recorded: 2026-09-08.

1. Identity​

FieldValue
Contractcatalog/v1alpha1
Realizationsindex (a directory of bundles plus one index file, no process), service (an adapter service answering the operations below over the ordinary wire)
Companion profileoperations/v1alpha1 generic-http; paged variant datasource.records/v1alpha1 generic-http-page; declared datasource.feed/v1alpha1 bindings (section 3.3)
Relation to operations/v1alpha1catalog operations are ordinary read operations; the catalog is optional and grants nothing (docs/design.md:39,147,325,1006)

A catalog answers three questions: which reviewed adapter definitions exist, with what provenance, and which artifact (by digest) realizes each. It never answers whether a caller may invoke anything; execution authority is unchanged whether a service was found directly or through a catalog (docs/design.md:1006). Bundles are data; the executable they name is separate trust (docs/design.md:808).

The pipeline this contract describes has three parts, matching the old repository's three parts: ingest (third-party or hand-authored declaration → pre-compiled bundle), distribution (index and service), and generic execution (a bundle's generic-http operations run by one engine). The adapter that hosts the last two is adapters/catalog/design.md.

3. Types​

3.1 Catalog operations​

catalog.providers.list (paged like datasource.records):

{ "limit": 100, "cursor": null }
{
"items": [
{
"adapter": "alertmanager",
"version": "0.1.0",
"kind_version": "connectors.adapter/v2",
"contracts": ["operations/v1alpha1", "datasource.records/v1alpha1"],
"operations": { "exposed": 1, "unresolved": 0, "refused": 0 },
"sources": [ { "origin": "vendor", "kind": "openapi", "url": "https://github.com/prometheus/alertmanager/blob/v0.31.0/api/v2/openapi.yaml", "revision": "v0.31.0", "sha256": "0fac1b1f…", "license": "Apache-2.0" } ],
"bundle": { "format": "connectors.generated-bundle/v1", "digest": "sha256:…" },
"toolchain": { "ess": "0.20.0", "rustfmt": "1.9.0-stable", "generator": "connectors-spec 0.1.0" }
}
],
"next_cursor": null,
"complete": true,
"provenance": { "instance": "catalog", "resource": "index", "observed_at_unix_ms": 0, "source_revision": "sha256:…" }
}

catalog.provider.describe { "adapter": "alertmanager" } → the bundle's descriptor (shape of adapters/kubernetes/generated/descriptor.json: adapter, configuration_schema, operations) plus a coverage summary per operation (generated, requires_implementation, refused, format connectors.import-coverage/v1) and the curation block per operation.

catalog.operations.list { "adapter": null, "contract": "operations/v1alpha1", "profile": "mutation", "effects": ["external_write"], "limit": 100, "cursor": null } → items:

{ "adapter": "zendesk", "id": "ticket.create", "contract": "operations/v1alpha1", "profile": "mutation",
"effects": ["external_write", "network"], "risk": "medium", "idempotency": { "kind": "none" },
"requires_auth": [{ "profile": "zendesk.api_token", "scopes": [] }], "realization": "generic" }

catalog.bundle.describe { "adapter": "zendesk", "digest": "sha256:…" } → manifest fields (format, specification_sha256, upstream_sha256, ess, rustfmt, files with per-file digests, as the v2 generator's manifest.json carries) plus locations: [ { "kind": "path" | "oci" | "https", "reference": "…" } ]. Bytes are not served over this contract; a host fetches by location and verifies the digest.

catalog.sources.status { "adapter": "zendesk" } → per source: pinned revision, sha256, fetched_at, refresh policy (manual, authored-review), and drift: "unknown" | "none" | "upstream_changed". drift is unknown unless a deliberate, separately invoked refresh check recorded a result; a read never contacts a vendor.

FieldRule
realizationgeneric (complete mapping, run by the engine), implemented (handwritten obligations satisfied in an adapter crate), unresolved (coverage lists requires_implementation or refused; never advertised as callable)
sources[].originvendor, repository-authored, vendor-derived, mixed (old vocabulary)
sources[].kindopenapi, swagger2, authored, asyncapi (recorded, not ingested)
bundle.digestSHA-256 over the manifest, which carries every file digest
toolchainexact versions from the manifest; a pin change requires regeneration (docs/development.md)

Errors: base codes; NotFound for an unknown adapter or digest; Unsupported for a bundle or index format version newer than the reader understands (refusal by name, old catalog-reader rule); Unavailable naming the file when a digest does not match; StaleCursor for an index generation that was replaced.

3.2 The generic-http profile​

A generic read carries profile: generic-http and realization: generic. A generic mutation carries the singular profile: mutation and realization: generic, including all mutation admission/observation rules. The following is an illustrative extended read descriptor; profile availability remains subject to the auth vocabulary owner:

{ "id": "alerts.list", "description": "Read alerts", "contract": "operations/v1alpha1", "profile": "generic-http", "realization": "generic",
"effects": ["network"], "semantic_effects": [], "risk": "low", "idempotency": { "kind": "natural" }, "approval": "not_required",
"limits": { "request_bytes": 262144, "result_bytes": 4194304, "execution_ms": 40000, "provider_ms": 30000, "connect_ms": 5000 },
"requires_auth": [{ "profile": "alertmanager.configured", "scopes": [] }],
"input_schema": { "type": "object", "properties": { "active": { "type": "boolean" } }, "additionalProperties": false },
"output_schema": { "type": "object", "required": ["status", "body", "provenance"] } }

Outcome: status (provider HTTP status), body (provider JSON, bounded, passed through unchanged), provenance (instance, resource = the mapping's path with parameters redacted to their names, observed_at_unix_ms, source_revision = the bundle digest).

Status to safe ErrorCode for generic reads only (base vocabulary in crates/connectors-core/src/lib.rs:13-27):

Provider resultCode
400, 422InvalidInput
401Unauthorized
403Forbidden
404, 410NotFound
409, 412InvalidInput
429RateLimited, retry_after_seconds from Retry-After when it is delta-seconds or an HTTP-date (whole seconds from 0 to 4294967295, a date rounded up and a past date 0); absent otherwise, and when two Retry-After field lines disagree (the transport combines repeated lines, RFC 9110 §5.3; identical repeats count as one)
5xx, connection refused, TLS failureUnavailable
deadline exceededTimeout
non-JSON body, oversize body, redirectUpstreamProtocol
2xx under a selection's declared success envelope whose success member is falsethe code the envelope's refusals give the body's error name, else its otherwise code (InvalidInput, Unauthorized, Forbidden, NotFound, RateLimited with retry_after_seconds from Retry-After, or Internal); never a success
2xx under a declared success envelope that is not a JSON object or whose success member is absent or not a booleanUpstreamProtocol
403 or 429 under a selection's declared rate-limit headers whose remaining header is exactly 0RateLimited, retry_after_seconds from Retry-After when present, else the seconds from now to the Unix time its reset header names (absent for a time already past or a value that is not whole seconds)
403 or 429 under a selection's declared rate-limit headers that carries Retry-After, whatever its remaining header says (a secondary limit)RateLimited, retry_after_seconds from Retry-After (absent for a value that names no delay)
2xx under a selection's declared embedded text: a file longer than its max_bytes, by the declared size or once decodedCapacity
2xx under a declared embedded text whose decoded bytes are not UTF-8 or hold a NUL byteUnsupported
2xx under a declared embedded text that is not a JSON object, or whose encoding is not base64 or whose content is absent or not base64UpstreamProtocol

The public Error.message is a host-selected safe classification, bounded to 512 UTF-8 bytes. Never copy raw provider messages, codes, response bodies, URLs or credential material into it. Declared error-envelope pointers may contribute to a reviewed operation-specific classifier; they do not authorize public disclosure or select a core code automatically.

For generic mutations, status alone does not prove business-effect knowledge. Use the mutation contract’s admitted not_attempted, proven refused, known applied, or conservative unknown observation. Ambiguous 5xx, lost responses, malformed or oversized replies after possible dispatch give outcome_unknown unless independent definitive evidence exists, with a safe secondary cause. A provider 409/412 is not the receiver’s idempotency_conflict; that code belongs exclusively to a conflict in its admitted key namespace. A write whose selection declares conflict_status (409 or 412, connectors_catalog.selection.ConflictStatus) is refused with the shared code conflict when the provider answers with that status: the provider says the target changed since the caller read it and nothing was applied, so the attempt is refused, never retried and never unknown. Without the declaration a 409 or 412 stays the InvalidInput refusal it was. Any definitive refusal mapping must prove no business effect under the selected operation semantics.

Profile selection is singular: a generic operation whose curation declares external_write selects mutation with realization: generic and all its rules. A mapping that declares pagination is exposed as datasource.records/v1alpha1 profile generic-http-page (items from the declared items pointer, cursor from the page or cursor parameter, complete from the declared end condition) instead of generic-http.

3.3 Declared feed bindings​

Status: implemented in adapters/catalog/src/feed.rs; held to the feed-binding suite by crates/connectors-build/src/catalog_feed_conformance.rs. GitLab declares the first shipped binding (adapters/catalog/contracts/feed/v1alpha1/gitlab.md), held to the suite by crates/connectors-build/src/gitlab_feed_conformance.rs. A Jira binding (adapters/catalog/contracts/feed/v1alpha1/jira.md) is declared in the shipped Jira selection, equal to the test fixture adapters/catalog/tests/feed/jira/feed.json, and held to the suite in five profile zones by crates/connectors-build/src/jira_feed_conformance.rs. A Confluence REST v1 binding (adapters/catalog/contracts/feed/v1alpha1/confluence-v1.md) is declared in the shipped confluence-v1 selection, equal to the test fixture adapters/catalog/tests/feed/confluence/feed.json, and held to the suite in five profile zones by crates/connectors-build/src/confluence_feed_conformance.rs.

A provider binds datasource.feed/v1alpha1 as data: the optional feed of its connectors-catalog-operations/1 selection file. The engine then answers feed.containers and feed.items, each with contract: datasource.feed/v1alpha1 and the declaration's profile, from operations of the provider's pinned bundle. No provider has Rust of its own. The declaration is modelled in adapters/catalog/spec/ess/domains/feed.yaml; a pointer is an RFC 6901 JSON pointer, a parameter a query parameter of the operation it is named for.

{
"profile": "<native profile id>",
"capabilities": {
"deletions": "observed | not-observed", "kind": "provider-word | fixed-word",
"revision": "opaque | update-time", "visibility": "mapped | all-private"
},
"max_limit": 100,
"containers": {
"operation_id": "<list>", "query": {"<name>": "<fixed value>"},
"limit": "<parameter>",
"cursor": {"parameter": "<parameter>",
"next": "<pointer>", "url_parameter": "<name>" | "last": "<pointer>" | "offset": {"complete": {"pointer": "<pointer>", "equals": "<JSON value>"}}},
"records": "<pointer>", "id": "<pointer>", "name": "<pointer>", "kind": "<pointer>" | "kind_word": "<word>",
"visibility": {"pointer": "<pointer>", "map": {"<provider value>": "public | private | direct"}},
"lookup": {"operation_id": "<get one>", "parameter": "<parameter>", "record": "<pointer>"}
},
"items": {
"operation_id": "<list>", "container": "<parameter>", "query": {}, "limit": "<parameter>",
"position": {"kind": "time", "value": {
"parameter": "<parameter>" | "query": {"parameter": "<parameter>", "language": "jql | cql",
"first": "<template>", "resume": "<template>",
"utc_offsets": {"least": <minutes>, "greatest": <minutes>}},
"precision": "instant | minute",
"page": {"parameter": "<parameter>", "next": "<pointer>", "url_parameter": "<name>"}}},
"records": "<pointer>", "id": "<pointer>", "revision": "<pointer>",
"created_at": "<pointer>", "updated_at": "<pointer>",
"author": {"id": "<pointer>", "display_name": "<pointer>"},
"body": {"pointer": "<pointer>", "representation": "<name>"},
"url": "<pointer>", "url_base": "<pointer>", "parent": "<pointer>",
"deleted": {"pointer": "<pointer>", "equals": "<JSON value>"}
}
}

query, name, visibility, author, display_name, url, url_base, parent, deleted, url_parameter, precision, page and utc_offsets are optional; a listing names exactly one of cursor.next, cursor.last and cursor.offset, url_parameter only beside a next, the containers exactly one of kind and kind_word (1 to 128 visible ASCII bytes), and a time position exactly one of parameter and query. items.container is the parameter that takes the container id, absent exactly when a query carries the id instead. Every named operation must be a GET under the base path, every named parameter one its operation takes, no parameter bound twice, every required parameter bound on every request, and a resume parameter not required; otherwise the selection file is refused when it loads, before any request.

ConcernRule
CapabilitiesRequired: the profile's capabilities in the family's vocabulary (datasource.feed/v1alpha1), each the one the rest of the declaration supports, or the selection file is refused when it loads. deletions: observed exactly when items.deleted is declared. kind: provider-word exactly when containers.kind reads the provider's word, kind: fixed-word exactly when containers.kind_word states the one word, which the suite reads as the family's kind_word, never repeated under capabilities. revision: update-time exactly when items.revision and items.updated_at are one pointer. visibility: mapped exactly when containers.visibility maps some value to public. The model's Declaration invariants state the same rules. The suite holds the binding to its claim.
ContainersOne provider page per call: limit (default max_limit) and the provider's cursor, which comes back at cursor.next. An absent, null or empty next is complete: true. With cursor.last instead, the provider lists after a key it is given, ordered by that key through query: a page of fewer than limit records is complete: true, and a full page continues after the value at last in its last record, a direct one included; a full page whose last record has none is unavailable, never complete. With cursor.offset, parameter takes the index of the page's first record: the next page starts after the records the answer carried, direct ones included, the answer for which offset.complete holds (such as isLast: true) is complete: true, and an empty answer for which it does not is unavailable; a cursor whose offset plus limit would pass 2^64 - 1 is stale_cursor, refused before any request. With url_parameter, the value at next is a URL, relative or absolute, and the continuation is that one query parameter of it, percent- and form-decoded; the URL is never requested and its host and path are not read, and a URL without that parameter, with it twice, or not decodable is unavailable. next_cursor wraps the provider's cursor, key or offset with the profile and the instance. kind is read at its pointer in each record; kind_word is the one word for every container.
Visibility and direct conversationsThe value at visibility.pointer, as text, is looked up in map; a value the map does not name, and a record with none, is private. A container mapped to direct is never listed. feed.items first reads the container through lookup; a 404 or 410 and a container mapped to direct both answer not_found with the same message.
Watermark: timeThe provider lists items at or after parameter (inclusive), oldest updated_at first; any fixed ordering parameter goes in query. The watermark is the last returned item's updated_at, sent back unchanged as that filter, and the (id, revision) pairs already returned at that instant, which are skipped on resume. At instant precision the engine asks for limit plus that many records, so a page of unseen items stays within reach; without page, a watermark holding max_limit pairs or more is no longer resumable, since no page can reach past them, and is stale_cursor, answered as the Watermark scope row says. An answer out of updated_at order, or before the filter, is unavailable. complete is true when the provider answered fewer records than asked and none was held back. With precision: minute the provider's filter has minute resolution: the engine sends the kept instant's minute in UTC and the provider re-reads that minute. A re-read item before the kept instant is dropped, since the watermark already holds it, and one at the kept instant is skipped by its pair, so the watermark still holds only the pairs returned at the kept instant, however many items share its minute. Each request of such a resume asks for max_limit items; order and the filter are checked from the start of that minute; and without page a resume whose one answer is full and holds no unseen item is unavailable. With query the filter, and the container id, are sent inside a query (language jql or cql) as query.parameter: first for a first read, holding the slot {container} once, and resume for a resume, holding {container} and {since} once each. Each slot's value is a quoted string literal of the language, " before and after and a \ before each " and \ in it, never spliced raw; a container id holding a control character is invalid_input, never sent. {since} is formatted from the parsed instant as "YYYY-MM-DD HH:MM" in UTC, and a watermark whose instant falls outside the UTC years 0000 to 9999 has no such literal and is stale_cursor, answered as the Watermark scope row says. With query.utc_offsets the provider reads {since} as wall time in a zone the engine is not told, such as the calling user's profile zone, whose offset lies between least and greatest minutes east of UTC, both included: each from -1439 to 1439, least not above greatest, and the time position read with page, or the selection file is refused when it loads. {since} is then the kept minute shifted by least (UTC-12:00 to UTC+14:00 is least: -720, greatest: 840, which sends the kept minute less 12 hours), so the provider's filter starts at or before the kept minute in every such zone and no item changed after it is skipped. A re-read item whose updated_at is before the kept instant is dropped, since the watermark already holds it; the order, and the filter, are checked from the earliest start those offsets allow, the kept minute shifted by least - greatest; each request of such a resume asks for max_limit items; and the shifted minute is the one whose UTC year must fall in 0000 to 9999. Such a resume re-reads up to greatest - least minutes before the kept minute, so one whose 10 pages hold no unseen item is unavailable, and stays so for that watermark (with 100-item pages, about 1000 items changed in that window before it). Without utc_offsets the provider reads {since} in UTC. A template is refused when the selection file loads if it holds another slot, a { or } outside a slot, a control character, more than 1024 bytes, a slot next to a ", ' or \, a slot inside a literal the template opens with " or ' (where a \ escapes the next character), a literal it leaves open, or a \ outside a literal. A query is at minute precision. With page, one call reads further provider pages, sending as page.parameter the token at page.next in the answer or, with page.url_parameter, that parameter of the URL there (read as for a listing), until it holds limit unseen items or an answer has no next; that continuation is never kept in the watermark. complete is then true when the last answer had no next and none was held back. At most 10 pages are read in one call; an incomplete call that would return no item is unavailable.
Watermark: cursorThe provider keeps a change cursor: parameter sends it, next reads the new one, complete is a condition on the answer. A cursor the provider refuses with a status listed in stale is stale_cursor. An answer with more records than limit is unavailable: its cursor is already past all of them.
Watermark scopeEvery watermark carries the format, the profile, the instance and the container. Any other, or one that is not a watermark, is stale_cursor, never a first read. Admission of the container comes before that answer, as the family requires: feed.items reads the container through lookup, then sends the item listing, as a first read when the watermark cannot be resumed. That is one not issued for this read, and one this binding issued that the declaration cannot resume: its {since} has no literal, or it holds, without page, max_limit pairs or more. Each is stale_cursor, as the family answers a watermark that is no longer resumable, and that answer comes only when that listing is answered, its records discarded, and a container the lookup or the listing refuses answers as the Errors row says, whatever the watermark. A first read omits the resume parameter and reaches as far back as the provider lists.
Itemsid and revision are required scalars; created_at and updated_at required instants, RFC 3339 or RFC 3339 with a ±hhmm offset (2026-10-06T09:00:00.000+0200). An RFC 3339 instant is returned as the provider wrote it; one with a ±hhmm offset is returned as the same instant in UTC, Z, its fraction kept. deleted is true when the value at its pointer equals equals; a tombstone has body: null. Without deleted the profile observes no deletions and declares deletions: not-observed. author is null when its id is absent. url is kept when it is http(s)://, and a path rooted at one / (no //, \, space or control character) resolves under a base: with url_base, the http(s):// URL at that pointer in the answer, with a non-empty host and without user information, a query or a fragment (such as https://example.com/wiki); without it, the origin of the instance's API base, as a root-relative reference resolves. Anything else, a relative path under an answer whose url_base is absent or unusable, and a relative path when the engine was given no API base or one whose host is not a hostname (such as a bracketed IP literal), is null; such an API base never stops the selection file from loading.
BodyA records body envelope with the declared representation; the text at body.pointer, null or absent as empty text, clipped at 64 KiB on a UTF-8 boundary (truncation: ["content_bytes"]). A non-text value is unavailable.
Ceilingslimit 1 to max_limit (at most 100), refused as invalid_input above it; watermark and listing cursor 16 KiB, a page whose watermark or cursor would pass it unavailable: a watermark carries the container id, which the items input bounds only as non-empty, and the pairs of items sharing its kept instant, and either can make it pass; a listing cursor carries the provider's cursor or key; serialized page 3 MiB (unavailable above it); feed.containers makes one provider request, feed.items two, or with page the lookup and at most 10 listing requests.
ErrorsProvider 400, 409, 412, 422 are invalid_input; 404, 410 not_found; 401, 403, 429 and 5xx as the generic read table, except that in feed.items, once the lookup has answered the container, a 403, 404 or 410 from the item listing that is not a quota refusal is not_found, with the message an unknown container gets: the connection can see the container but not read its items, which the family answers as for a container it cannot read; any other status and any body that is not JSON unavailable. Provenance is instance, profile and received_at.

What a declaration cannot express, each a reviewed extension of this shape rather than per-provider code:

  • container listings paged by page number or by a response header (Link); item listings paged by offset, by page number or by the last record's key, and a cursor position continued within one call;
  • a time filter that is exclusive, takes another format than the provider's own updated_at text or a minute in UTC, is sent in a body, or in a query language other than jql and cql; a query zone read from the provider's answers rather than bounded by utc_offsets, which would narrow the window a resume re-reads; a provider answering newest first with no way to ask otherwise;
  • an item endpoint per container type, or a container that is not one list request plus one lookup;
  • visibility from more than one field (a container's own and that of the feature its items belong to); direct containers declared readable;
  • a revision derived by the engine (a digest of the mapped fields) where the provider gives no version or edit time; deletions observed other than as a record the list returns;
  • a body built from several fields, or anything but text, and an author or parent that is not one scalar field;
  • binding a watermark to the connection rather than the instance: the engine is not told which connection it reads through.

3.4 Alternative bindings​

Status: implemented in adapters/catalog/src/local.rs; held by adapters/catalog/tests/confluence_binding.rs. Confluence declares the first configured use: REST v2 (confluence) first, REST v1 (confluence-v1) its alternative (docs/catalog-confluence.md).

One instance whose credential may read one of several APIs of the same base declares them as data: the optional bindings of its connectors-catalog-local/2 configuration. The configuration's own provider, selection set and auth.access are the first binding; each alternative names another bundle (by provider, from the same bundle_directory), its selection set and its access probe. The engine names no provider. The declaration is modelled in adapters/catalog/spec/ess/domains/binding.yaml.

{
"bindings": {
"id": "<first binding>",
"next": {"status": 401, "reason": "<text in the admitted reason>"},
"alternatives": [
{"id": "<binding>", "provider": "<bundle>", "operations_file": "<selection set>",
"access": {"path": "<read>", "query": {"<name>": "<value>"}},
"next": {"status": 401, "reason": "<text>"}}
]
}
}
ConcernRule
ShapeIds are unique; next is required before every alternative and refused after the last; its status is 401 or 403, the refusals the Refusal row counts, and any other bindings.next.status or alternative next.status is refused. The first binding needs auth.access, every alternative its access. Every selection set is read only: a configuration with bindings serves no write. Alternatives share the base, the hosts, the authentication profile and the credential. Anything else is refused when the configuration loads.
DescriptionEach binding is its own descriptor and requirements; their revisions differ, each digesting its binding id. The private bootstrap describes the first binding as before and carries the others under bindings (private adapter binding). The bindings enter the configuration revision; a configuration without them keeps its revision.
ValidationThe identity probe runs once, and its refusal stands: a wrong password is invalid_credential, never retried under another binding. The scope read follows, then the first binding's access probe. A 2xx binds there. A refusal whose status equals next.status and whose admitted reason (connectors_core::reason) contains next.reason, case-insensitively, moves on to the next binding's probe; any other refusal stands. Every validation starts again from the first binding, so a revalidation may select another.
RefusalWhen every binding tried is refused with 401 or 403, validation is insufficient_scope with one reason naming each answer in order, binding <id> answered <status> (<admitted reason>) joined by ; , admitted again and bounded to 256 bytes. Any other status is the probe's failure as without bindings.
ReadsA read names the binding its connection's evidence selected. An id that binding does not serve is not_found before any request; a revision of another binding is stale_description.

4. Rules​

  • Optional: a directly configured service is fully usable without an index or a catalog service (docs/design.md:325,1006).
  • Grants nothing: no operation here returns a credential, a credential address, an admission decision, or an endpoint the client must trust; the client's trust decision is its own (docs/design.md:325).
  • Data, not executable: a bundle names its realization artifact by digest; installing or running it is explicit configuration (docs/design.md:808).
  • Truthful descriptors: only generic and implemented operations appear in a descriptor; an unresolved operation is listed by the catalog and absent from the service (docs/design.md:311,894).
  • Curation is mandatory: an imported operation without expose, effects, risk and idempotency is unresolved, because OpenAPI supplies none of them (docs/design.md:900-906).
  • Provenance is complete: every bundle records origin, exact URL and revision, SHA-256 of the original bytes, every transform, and the vendor license file where one exists (adapters/gitlab/upstream/LICENSE, spec-kinds/adapter/v2/semantics.md:16-18, in history at commit 6498a881c).
  • Hermetic reads: catalog operations and generation never contact a vendor; refresh is a separate, deliberate, reviewed action (docs/design.md:292; old catalog-build invariant).
  • Deterministic: identical inputs and toolchain produce identical bundles and an identical index digest (spec-kinds/adapter/v2/semantics.md:52-55, in history at commit 6498a881c).
  • Refusal by name: an unknown bundle or index format version is refused before any record is served.
  • Engine discipline: the request is built only from the mapping, the validated input, and the host-injected credential capability; the caller supplies no URL, method, header, or path; body comes from the declared template with $param splices only; the response is bounded; redirects are not followed; one hop.
  • Unary only: the generic profile serves unary HTTP operations; datasources beyond generic-http-page and a declared datasource.feed/v1alpha1 binding (section 3.3), events, channels, discoveries and acquisition are not served by the engine (old integration-catalog exclusions preserved).
  • Public authentication metadata: bundle files, descriptors, indexes and catalog outputs may name safe auth-profile identifiers and configuration requirements, but contain neither credential values nor actual runtime credential locators (custody/version references, environment-variable names, filesystem paths or secret-store addresses). A schema may describe a protected configuration slot; it cannot populate that slot with a deployment's credential location. The receiving host owns those private bindings and injects an admitted capability at runtime. Reject an artifact violating this boundary before publication or serving; value freedom alone is insufficient.

5. Ordering and limits​

ConcernRule
Page1–100 as datasource.records
Index sizeunbounded, paged; one generation at a time
Generic request256 KiB including the extended envelope; selected first-profile ceiling
Generic result4 MiB including envelope and metadata; a provider body too large to fit gives safe UpstreamProtocol for reads; mutations preserve actual effect knowledge
Generic deadlines40 s total execution, 30 s provider within it, 5 s connect within provider time; advertised through Operation.limits. No independent budget reset or silent clipping to the legacy 20 s execution limit
Retrynone by the engine: it sends exactly one request. The local catalog provider sends a read answered 429, or refused RateLimited by its declared success envelope, once more after the retry_after_seconds it names, never a third time, and only when the wait, a second request as long as the first one took and a 500 ms margin all end before the invocation deadline. The second request is bounded by the deadline less 500 ms; if it does not finish, the first answer's RateLimited with its retry_after_seconds stands; otherwise, and for every write, RateLimited carries retry_after_seconds for the caller
Refreshnever on read; only by the refresh tool

The service compatibility limits own envelope accounting and admission. These new generic limits are not implemented legacy host capabilities. Catalog inventory reads use the ordinary read limits; only generic execution selects the generic ceilings.

6. Conformance scenarios (docs/design.md:1006)​

  • Catalog independence: configure a fixture service directly, discover its contracts, invoke a capability; then discover the same service through a catalog; admission outcome and invocation result are identical.
  • Coverage truth: a bundle with one operation under requires_implementation → catalog.operations.list shows it unresolved; the descriptor omits it; invoking it yields the base unknown-operation error.
  • Curation truth: an imported operation without risk → unresolved; adding the curation block makes it generic.
  • Tamper: change one byte of a bundle file → catalog.bundle.describe returns Unavailable naming the file; the index generation is not served.
  • Format refusal: an index with format version v9 → Unsupported before any record is served.
  • Credential-location refusal: a bundle/descriptor/index containing { "kind": "environment", "name": "ZENDESK_TOKEN" } or a real custody/file locator → refused before publication or catalog disclosure, even though it contains no secret bytes. A safe auth-profile requirement and an unpopulated protected-slot schema are permitted; they grant no credential access.
  • Read engine: fixture 200 with JSON → body structurally identical, provenance.resource carries parameter names not values; 429 with Retry-After: 7, or with an HTTP-date seven seconds ahead → RateLimited, retry_after_seconds: 7, one request; 429 with Retry-After: soon or none → RateLimited without retry_after_seconds; 404 → NotFound; 500 → Unavailable; text/html body → UpstreamProtocol; body that exceeds the total response budget after metadata/envelope accounting → UpstreamProtocol; 302 → UpstreamProtocol and no second request.
  • Engine discipline: an input containing a URL-shaped string in a path parameter is percent-encoded into its segment; the fixture observes exactly one request to the declared path.
  • Paged: a mapping with pagination.page → generic-http-page returns complete: true when the fixture returns fewer items than the page size; the cursor is opaque and bound to the input digest.
  • No network: run generation and every catalog operation with a network sentinel; zero connections (old tests/main/no_network.rs shape).
  • Determinism: generate the same adapter twice with the pinned toolchain; manifests and index digest are identical.
  • Public authentication metadata: serialize every catalog output and bundle file; neither the fixture's secret token nor its actual environment/custody/file locator may appear. Safe profile requirements remain distinguishable from private runtime bindings.
  • Alternative bindings (section 3.4): the first binding refused with its next answer and the second answered → bound to the second, and a read under it returns its answer; the first answered → bound to it, no request to the second's probe; both refused → insufficient_scope naming both answers; a wrong password → invalid_credential after the identity read alone; a first-binding refusal that is not its next → that refusal, no second probe; a later validation that sees the first answering → bound to it; an id the bound binding lacks → not_found, no request.

7. Compatibility​

  • New contract; new profile strings generic-http and generic-http-page.
  • connectors-catalog-operations/1 gains the optional feed (section 3.3). A selection file without it loads, and its configuration revision is computed, exactly as before; one with it is refused by a reader older than this section, which denies unknown fields.
  • A selection gains the optional query_fixed and query_folds (a query value the caller cannot send, and a caller input folded into it, connectors_catalog.selection.FixedQuery), embedded_text (a JSON answer's base64 file read as text, connectors_catalog.selection.EmbeddedText) and rate_limit_headers (connectors_catalog.selection.RateLimitHeaders), each owned by the catalog provider guide. Each is omitted when absent, so every selection without it keeps its bytes and its configuration revision; one with it is refused by an older reader, which denies unknown fields.
  • A write's closed body keys may be RFC 6901 pointers into nested objects (/ticket/comment/body), a plain name meaning the top-level key as before, and body_types gains string_list. A selection gains the optional body_enums, body_any_of and body_prefixes (connectors_catalog.selection.ClosedBody) and conflict_status (connectors_catalog.selection.ConflictStatus), and a guard preflight gains the optional mismatch (connectors_catalog.guard.MismatchCode: a differing check refused as conflict rather than forbidden), owned by the same guide. Each is omitted when absent, so every selection without it keeps its bytes and its configuration revision; an older reader refuses one with it, which it denies as an unknown field, and reads a pointer key as a literal top-level name.
  • connectors-catalog-local/2 gains the optional bindings (section 3.4). A configuration without it loads, and its configuration revision, bootstrap and validation are computed, exactly as before; one with it is refused by an older reader, which denies unknown fields, and needs a host that reads the bootstrap's bindings and the baseline's binding.
  • A guard check's expectation gains {"present": true} (connectors_catalog.guard.Expectation): the member exists and is not null, whatever its value, owned by the catalog provider guide. A selection without it keeps its bytes and its configuration revision; an older reader refuses one with it as an unknown variant.
  • Service compatibility owns the extended descriptor fields, singular profile, auth alternatives and generic limits. These require the proposed v1alpha2 codec. Index, adapter-kind, bundle and configuration readers retain their own independent version decisions; a service-wire bump does not extend them.
  • Old catalog/<id>.catalog.json documents and catalog.pack bytes are not preserved (docs/design.md:911). Old operation ids are preserved only where the migration contract selects them (docs/design.md:1095).
  • The predecessor's published catalog-reader crate has no successor in this design; its consumers need an explicit migration decision (docs/design.md:151).

8. SDK and host obligations​

ObligationWhere
Specification kind: mapping vocabulary extended with any method, JSON body template with $param splices, header parameters, error envelope pointers, pagination, rate limit; curation block per operation; upstream optional for authored sources; Swagger 2.0 exact projection recorded as a transformspec-kinds/adapter/v2/schema.json (mappings items today: method, path, path_parameters, query_parameters, obligations; upstream required), crates/connectors-spec/src/ (both in history at commit 6498a881c)
Generator: default prepare and finish for generic-http; today both are required handwritten methods (spec-kinds/adapter/v2/semantics.md:20-21, in history at commit 6498a881c)crates/connectors-spec (in history at commit 6498a881c)
Bundle format: add sources[], curation, realization, license to the manifest (format today connectors.generated-bundle/v1, fields ess, files, rustfmt, specification_sha256, upstream_sha256)adapters/*/generated/manifest.json
Index tool: catalog build, check, diff over every adapters/*/spec; sources refresh as the only networked verb, never in build.rscrates/connectors-build/src/main.rs (today: local executor only, line 1)
Generic engine: mapping + validated input + capability → request; response → outcome; the catalog adapter links it, no provider crate doesnew crate under crates/, name in adapters/catalog/design.md §10
AuthenticatedHttp with method, body and headers; http-header and http-query credential placements (old declarations: 17 named headers, 2 query parameters)crates/connectors-sdk/src/lib.rs:53-56; contracts/auth/capability/v1alpha1/semantics.md (reserved/unbound until the vocabulary and auth owners define support; no current descriptor may advertise it)
Client: typed catalog.* calls and bundle locate-and-verifycrates/connectors-client
Wire v1alpha2 descriptor fieldscrates/connectors-core/src/lib.rs:70-78

9. ESS declarations and optional catalog state​

The shared typed home for immutable artifact provenance is artifact_provenance.yaml. These records describe pinned declarations and bytes independently of any catalog service. Existing AdapterSpecification.adapter_id, operation identities, UpstreamSource and current strict readers retain their meanings. This model does not make the proposed curation/source fields accepted by a current reader.

DeclarationSelected identity and relation
Existing AdapterSpecificationExisting logical adapter id; references zero or more reusable Source records through source_refs; its operation ownership is unchanged
SourceImmutable record identified by an opaque source_ref for the exact (url, revision) tuple; origin, kind, original-byte SHA-256, retained license evidence and ordered transforms
BundleImmutable digest of the exact manifest bytes; references one logical AdapterSpecification plus its separately pinned specification SHA-256, and zero or more Source records
CurationOptional embedded authored value on OperationDeclaration: expose, executable/semantic effects, risk, idempotency and realization. Absence remains unresolved for profiles requiring curation
CatalogIndexDeferred adapter-owned publication selection. Its digest identifies one index generation; membership references reusable immutable Bundles and does not own their deletion
Tenancy, index publication authority and runtime retention/capacityDeferred; no selected catalog service or publishing lifecycle is implemented or declared in shared ESS

A Source key is an injective owner-maintained association with the exact URL and revision, not an ambiguous concatenation or display label. A second observation of that tuple with different pinned bytes or provenance is a conflicting source record and refuses replacement; it requires an explicit reviewed source revision. References never silently follow a moving source. Repository-authored inputs use their recorded repository source and revision and do not invent vendor provenance. The original-byte digest and each transform's ordered input/output digest remain distinct. A transformed document cannot relabel its output as the original vendor bytes. License evidence records retained source/license files where available; an absent record is not a license grant or proof that no license exists.

Source reuse is many-to-many through the explicit AdapterSpecification/Bundle reference lists: no adapter or bundle owns a Source's lifetime. The lists identify a particular immutable declaration, not every historical revision under the same logical adapter id. A Bundle's logical adapter reference alone cannot establish its exact specification revision; its pinned specification digest and retained bytes do. No existing identity is silently redefined to mean a revision key.

The Source lifecycle is the single immutable Recorded state and Bundle the single immutable Generated state. These states classify retained declarations, not whether a deployment installed them or an index currently selects them. Multiple indexes or consumers may select the same Bundle digest. Replacing one index retires that selection and does not mutate or delete the shared Bundle or Source. The earlier proposed generated → indexed → superseded Bundle lifecycle and CatalogIndex owns Bundle edge are superseded by this reuse rule. No automatic deletion or cascade is selected by this model; later packaging/catalog retirement must prove no remaining retained consumer or safety obligation.

Curation records author decisions; it does not discharge implementation obligations or make unresolved work callable. Generic execution and new curation or manifest syntax require their separately reviewed reader/schema binding. Unknown/reserved realization, effect, risk or idempotency meanings refuse under that binding; an optional ESS field is not an extension to a strict public reader. The existing native operation profile/discriminator still selects mutation versus read behavior.

ESS validates declared field types and explicit references. It does not enforce tuple uniqueness, digest computation, source/license truth, manifest path safety, cross-field curation completeness, current publication authority, file retention or deterministic generation. Those remain named binding predicates; no storage, catalog service, generator or public codec was implemented for this model. The optional CatalogIndex publication lifecycle belongs under the catalog adapter when selected, not in the shared provider-independent domain.

10. Open decisions​

DecisionDefault taken
How a catalogued operation executes: (A) one generic executable loading bundles as data; (B) generated Rust crate per provider; (C) A for generic-http, B for operations bound to a semantic contractC
Bundle transportdirectory plus index first; locations.kind = oci reserved (docs/design.md:805)
Where the generic engine livesin the catalog adapter, an ordinary provider (docs/design.md:39); not in the host
Swagger 2.0exact projection to OpenAPI 3.0 at ingest, recorded as a named transform
AsyncAPI sources (2 old, specs/b10x.provenance.toml:73,95)recorded in sources[], not ingested
Old operation-id compatibilitynot preserved by default; selected per adapter at curation
Successor for the published reader cratenone; decision needed before any external consumer migrates