CX · Reference guideconnector
PlaygroundDownloadsAboutv0.18.0-pre.1

connector

The connector kit: one engine every connector feature runs on, and the vocabulary that declaration is written in. It is an OPEN ADAPTER CONTRACT: the engine core owns the walk's totality and its one stop rule, the budget arithmetic and its two ledgers, classification into the closed retryable set, the credential chain, the audit record's core, failure isolation and the bounds ceiling, while each transport KIND registers a row declaring its own gateway schema fragment, its own pagination styles, its own classification table, its own audit detail vocabulary, its own discovery, its own capability and bounds and its own deployment-binding fields — which the core then enforces generically, so a new protocol, database or approach is a row plus its own verbs and touches none of the engine. http and db are the kinds registered in this release — db is a database as a transport over core/db_access, with a cursor COLUMN for its walk, the backend's own state code for its classifier, a schema for the reach its confine holds, and the isolation level first-class on the declaration, on the call and in the record; a gateway naming an unregistered kind refuses CXER6326 at plan, run, walk and describe. A connector IS a feature — a kind=feature package whose grammar is an external system's nouns and verbs, whose [source] [gateway] rows are its transports, whose apply delegates here and whose readout folds what it ingested. There is no connector document, no fourth package kind and no second install gate. What this module owns is the part that is hard to write correctly a thousand times: the WALK (four closed pagination styles — cursor, page, offset and RFC 8288 Link — with ONE stop rule, a required max-pages=, and a cursor cycle refused rather than looped); the CLASSIFICATION of an HTTP non-2xx into an [err … retry-after=] the shipped [?retry] can read, with a vendor's Retry-After normalized, clamped at five minutes and dropped when it is malformed; the BUDGET arithmetic of whose window a call spends, as a sliding half-open window over a ledger; the machine-token CADENCE over oidc:client-credentials, re-minted and never refreshed; declarative request SIGNING by scheme name; bounded schema DISCOVERY; the declaration CHECKER; and the pure build-time transform OpenAPI document -> connector feature package skeleton that never infers an idempotency claim. Twelve of its eighteen verbs are Ring 1 and pure — every decision the kit makes lives in that half — so the eight-class attack corpus (auth-header injection, credential smuggling, pagination loops and cursor cycles, Retry-After abuse, rate-limit window edges, schema bombs, host escape, ingestion hostility) grades with no socket, no clock and no capability. The engine writes NO resilience: retry, backoff, jitter, rate limiting, circuit breaking and timeouts are the shipped directives of code.md §10.2, declared around the call in the spec'd nesting order and implemented nowhere here. The SHAPE is in the feature and the BINDING is in the deployment: a gateway declares no base URL, no credential, no retry policy and no schedule, and a credential VALUE never appears in a document, a URL, a log line or an audit record.

connector:plan

[$connector:plan] ($gateway::element $verb::element $binding::element $intent::element) -> element — Build the [request] for one verb — method, URL, query, headers and body — purely. The auth scheme contributes an [auth scheme= handle=] marker child and NEVER a value, so a planned request cannot carry a secret even in principle.

      [?lib 'cx-platform/connector']
[?let [= $g [gateway name=acme-api kind=http auth=client-credentials]]
      [= $v [verb name=list-contacts effect=observe via=acme-api path='/v3/contacts' [intent [do :list-contacts]]]]
      [= $b [binding base-url='https://api.example.test' credential='handle:acme/prod']]
      [= $req [$connector:plan $g $v $b [do 'acme/list-contacts']]]
      [= $a [$first $req/auth]]
  [probe [scheme $a@scheme] [handle [$string $a@handle]] [headers [$count $req/headers/*]]]]
    
      [probe [scheme :client-credentials] [handle handle:acme/prod] [headers 0]]
    

connector:paginate-of

[$connector:paginate-of] ($gateway::element $verb::element) -> any — Answer the EFFECTIVE [paginate] for one verb: the verb's own where it carries one, otherwise the gateway's, otherwise the absence channel — resolved in one place so three callers cannot resolve it three ways.

      [?lib 'cx-platform/connector']
[?let [= $g [gateway name=acme-api kind=http auth=bearer
              [paginate style=cursor max-pages=50 path='/next_cursor' param=cursor]]]
      [= $v [verb name=list-contacts effect=observe via=acme-api path='/v3/contacts' [intent [do :list-contacts]]]]
      [= $p [$connector:paginate-of $g $v]]
  [probe [style $p@style] [max-pages $p@max-pages]]]
    
      [probe [style :cursor] [max-pages 50]]
    

connector:walk-begin

[$connector:walk-begin] ($paginate::element $request::element) -> element — Build the first [walk-state] for a [paginate] declaration and a first request; a declaration that does not validate refuses CXER6301.

      [?lib 'cx-platform/connector']
[?let [= $p [paginate style=cursor max-pages=10 path='/next_cursor' param=cursor]]
      [= $st [$connector:walk-begin $p [request method=GET url='https://api.example.test/v3/contacts']]]
  [probe [page $st@page] [cursor [$string $st@cursor]] [seen [$count $st/seen/*]]]]
    
      [probe [page 1] [cursor ''] [seen 0]]
    

connector:walk-step

[$connector:walk-step] ($paginate::element $state::element $response::element) -> element — The whole of the walk's decision, purely: continue with a new request, or stop with one of the four named reasons — and refuse a continuation that would change the host, forge a header or break a bound.

      [?lib 'cx-platform/connector']
[?let [= $p [paginate style=cursor max-pages=10 path='/next_cursor' param=cursor]]
      [= $st [$connector:walk-begin $p [request method=GET url='https://api.example.test/v3/contacts']]]
      [= $r [response status=200 [body '{"next_cursor":"c2","items":[{"id":1}]}']]]
      [= $s [$connector:walk-step $p $st $r]]
  [probe [action $s@action] [url [$string [$first $s/request]@url]]]]
    
      [probe [action :continue] [url https://api.example.test/v3/contacts?cursor=c2]]
    

connector:classify

[$connector:classify] ($response::element $now::datetime $opts::map {}) -> any — Turn a [response] into a value or an [err … retry-after=] — the function the shipped [?retry] [on …] predicate reads, which is the only reason a retry loop ever sees a 429 at all.

      [?lib 'cx-platform/connector']
[?lib 'cx-stdlib/time']
[?let [= $r [response status=429 [headers [header name="Retry-After" value="2"]]]]
      [= $e [$connector:classify $r [$time:datetime 2026 9 13 10 0 0]]]
  [probe [code [$string $e@code]] [status $e@status] [retry-after [$string $e@retry-after]]]]
    
      [probe [code cx-err:CXER6307] [status 429] [retry-after '2s']]
    

connector:sign

[$connector:sign] ($request::element $scheme::string $key::bytes $now::datetime) -> element — Sign a request by SCHEME NAME — sigv4 or hmac-sha256-header. Pure over bytes: the key is the caller's, is never read from a binding and is never rendered.

connector:budget

[$connector:budget] ($limits::element $ledger::element $now::datetime) -> element — Answer whether the next call may go and, if not, when — a sliding half-open window over a tenant's or a vendor's ledger, with retry-after= rounded UP so a retry is never refused again.

      [?lib 'cx-platform/connector']
[?lib 'cx-stdlib/time']
[?let [= $l [limits max=2 per='60s' scope=tenant]]
      [= $ledger [ledger [at '2026-09-13T09:59:00Z'] [at '2026-09-13T09:59:30Z']]]
      [= $b [$connector:budget $l $ledger [$time:datetime 2026 9 13 10 0 0]]]
  [probe [allow $b@allow]]]
    
      [probe [allow true]]
    

connector:token-fresh

[$connector:token-fresh] ($tokens::element $now::datetime $skew::duration) -> bool — Decide purely whether a cached machine token is still usable given $now and the deployment's skew — so a token-cadence bug is a fixture rather than a production incident.

      [?lib 'cx-platform/connector']
[?lib 'cx-stdlib/time']
[$connector:token-fresh [tokens access='t' token-type='Bearer' expires-at='2026-09-13T10:05:00Z']
                         [$time:datetime 2026 9 13 10 0 0] '60s']
    
      true
    

connector:schema-of

[$connector:schema-of] ($response::element $opts::map {}) -> element — Turn a fetched schema document into a [schema] element, with every §8 bound — bytes, depth, nodes, $ref expansions — enforced HERE rather than at the socket, which is why a schema bomb is graded with no network.

      [?lib 'cx-platform/connector']
[?let [= $s [$connector:schema-of
              [response status=200
                [headers [header name="Content-Type" value="application/schema+json"]]
                [body '{"type":"object","properties":{"id":{"type":"string"},"custom_17":{"type":"string"}}}']]]]
  [probe [head [$name $s]] [fields [$count $s/field]]]]
    
      [probe [head schema] [fields 2]]
    

connector:audit-record

[$connector:audit-record] ($request::element $outcome $facts::map) -> element — Build the one [audit] record for one outbound call: the subject= with its query string stripped, the decision= derived from the outcome, and this module's own [outbound] detail — with no body, in either direction.

      [?lib 'cx-platform/connector']
[?let [= $a [$connector:audit-record
              [request method=GET url='https://api.example.test/v3/contacts?cursor=c2&api_key=s3cr3t']
              [response status=200]
              {gateway: 'acme-api', verb: 'list-contacts', tenant: 'tenant-17'}]]
  [$string $a@subject]]
    
      'https://api.example.test/v3/contacts'
    

connector:validate

[$connector:validate] ($source::element) -> element — REPORT every fault a declaration carries as [findings] data an author, a linter or a gate reads. It never refuses: the refusals happen at open, where a deployment is fixed before a customer sees it.

      [?lib 'cx-platform/connector']
[?let [= $f [$connector:validate [gateway name=acme-api kind=http auth=magic]]]
  [$string [$first $f/finding]@code]]
    
      'cx-err:CXER6308'
    

connector:ingest

[$connector:ingest] ($openapi::element $opts::map {}) -> element — The pure build-time transform OpenAPI 3.1 document -> connector feature package skeleton. It NEVER infers an idempotency claim: every act verb carries an authoring TODO instead, and a person who read the vendor's documentation sets the attribute.

      [?lib 'cx-platform/connector']
[?let [= $p [$connector:ingest [openapi version='3.1.0'
              [info title='Acme' version='1']
              [paths [path route='/contacts'
                       [operation method=GET id='listContacts' summary='List contacts']]]]]]
      [= $v $p/feature/verbs/verb]
  [probe [name [$string $v@name]] [effect $v@effect]]]
    
      [probe [name list-contacts] [effect observe]]
    

connector:ingest-diff

[$connector:ingest-diff] ($pinned::element $fresh::element $opts::map {}) -> element — What a vendor's fresh description changes against the PINNED package — verbs, nouns and fields added, dropped, renamed or retyped, the auth scheme and the walk — and which of the package's authored edits (an idempotency claim first) the change invalidates. Pure: the version loop's report, changing no package.

      [?lib 'cx-platform/connector']
[$connector:ingest-diff
  [feature name=acme version='1' [verbs [verb name=list-contacts effect=observe via=acme-api [intent [do :list-contacts]]]]]
  [openapi version='3.1.0' [info title='Acme' version='2']
    [paths [path route='/contacts' [operation method=GET id='listContacts' summary='List'
                                     [parameter name='limit' in='query' type='integer']]]]]]
    
      [ingest-diff from='1' to='2' [verbs [changed name=list-contacts [slot-added name=limit]]] [nouns] [auth] [paginate] [invalidated]]
    

connector:bind

[$connector:bind] ($package::element $env::string $opts::map {}) -> element — The deployment binding scaffold (§4.6) for a package and an environment: one [gateway] per declared gateway carrying every field its kind's adapter row declares, the tenant's credential and both budget rows — every value the atom :todo, which open refuses until the operator fills it. The operator fills values, never shapes.

      [?lib 'cx-platform/connector']
[$connector:bind [feature name=acme version='1' [source [gateway name=acme-db kind=db]]] 'production']
    
      [deployment name=acme-production [runtime [connectors [connector feature=acme [gateway name=acme-db handle=:todo schema=:todo [budget max=:todo per=:todo]] [tenant name=:todo [credential gateway=acme-db handle=:todo] [budget gateway=acme-db max=:todo per=:todo]]]]]]
    

connector:mock-answer

[$connector:mock-answer] ($mock::element $request::element) -> element — The mock system's answer to ONE request: the first [route] whose method and path template match answers its documented example as JSON; a route the description gave no example answers 501 naming the TODO, and no route 404. Pure, so a test build mounts the whole mock under one catch-all [?http-service] resource and grades a connector with no vendor sandbox.

      [?lib 'cx-platform/connector']
[?let [= $m [mock-system name=acme
              [route verb=list-contacts method=GET path='/contacts' [respond status=200 example='{"items":[]}']]
              [route verb=get-contact method=GET path='/contacts/{id}' [respond status=200 example='{"id":"c-1"}']]
              [route verb=create-contact method=POST path='/contacts' [todo about=example]]]]
  ([$connector:mock-answer $m [request method=GET path='/contacts?updated_since=2026-01-01']],
   [$connector:mock-answer $m [request method=get path='/contacts/c-9']],
   [$connector:mock-answer $m [request method=POST path='/contacts']],
   [$connector:mock-answer $m [request method=DELETE path='/contacts']])]
    
      ([response status=200 [headers [header name=Content-Type value='application/json']] [body '{"items":[]}']], [response status=200 [headers [header name=Content-Type value='application/json']] [body '{"id":"c-1"}']], [response status=501 [headers [header name=Content-Type value='application/json']] [body '{"todo":"example","verb":"create-contact"}']], [response status=404 [headers [header name=Content-Type value='application/json']] [body '{"error":"no route for DELETE /contacts in the mock system"}']])
    

connector:open

[$connector:open] ($host::element $feature::string $opts::map {}) -> element — Bind a feature's gateways to this deployment and answer a live handle. Idempotent per (deployment, feature, tenant), and the place EVERY boot-time refusal happens — an unbound credential, a value in a declaration, an invalid gateway, a raised bound, an unbound audit sink.

      [?lib 'cx-platform/connector']
[$connector:open [host tenant='acme'] 'acme']
    
      cx-err:CXER0271
    

connector:close

[$connector:close] ($connector::element) -> null — Release a handle, its token cache and its client. Idempotent: closing a closed handle is null, never an error.

connector:run

[$connector:run] ($connector::element $verb::string $intent::element $opts::map {}) -> element — Execute ONE call: resolve, plan, check the budget, authorize, send inside the declared rate-limit / timeout / retry / circuit-breaker envelope, classify, and record. One call, one audit record.

      [?lib 'cx-platform/connector']
[$connector:run [connector feature='acme' tenant='acme'] 'acme/list-contacts' [do 'acme/list-contacts']]
    
      cx-err:CXER0271
    

connector:walk

[$connector:walk] ($connector::element $verb::string $intent::element $opts::map {}) -> any — Walk a paginated verb, yielding pages one at a time; the bound and cycle errs arrive on the failure channel AFTER the last page, because a bounded walk that hid its bound would be a silent truncation.

      [?lib 'cx-platform/connector']
[$connector:walk [connector feature='acme' tenant='acme'] 'acme/list-contacts' [do 'acme/list-contacts']]
    
      cx-err:CXER0271
    

connector:log-install

[$connector:log-install] ($connector::element $verb::string $opts::map {}) -> element — Install the change table a sqlite verb's [capture mode=log log=<name>] names (§3.8.10, sync.md §3.4): the table — cx_seq INTEGER PRIMARY KEY AUTOINCREMENT, cx_op, and every column of the verb's target= table — and its three AFTER triggers (insert and update log NEW's row, delete OLD's), each CREATE IF NOT EXISTS; then a read-back of the text the backend stores for all four, refusing CXER6300 naming the one that differs (a captured table that gained a column); then the baseline — a marker row and one insert row per row the table holds — written once, or anew under {baseline: "fresh"}. Every statement is the declaration's and the catalog's, never a caller's, and each is one call through the envelope with its own record. Answers [log-surface backend= schema= table= log= key= columns= baseline=N]. On backend=postgres (§3.8.10's postgres half) it makes instead the PUBLICATION of the target= table and the logical replication SLOT decoded by pgoutput, both named by log= (a slot name: [a-z_][a-z0-9_]*), each only where none is held, after setting the table's REPLICA IDENTITY FULL; reads back the slot's plugin, type and database, the publication's tables, columns, row filter and operations and the identity, refusing CXER6300 naming the one that differs; {baseline: "fresh"} first drops a slot whose WAL is lost. It answers [log-surface backend=:postgres … baseline=0 slot=created|held publication=created|held] — the postgres baseline is read, not written (log-read). On backend=mysql (§3.8.10's mysql half) it makes nothing: it reads the captured table's columns from information_schema and the server's four logging variables back (log_bin ON, binlog_format ROW, binlog_row_image FULL, binlog_row_metadata FULL), refusing CXER6300 naming the first that differs, and answers [log-surface backend=:mysql … baseline=0].

      [?lib 'cx-platform/connector']
[$connector:log-install [connector feature='acme' tenant='acme'] 'acme/list-orders']
    
      cx-err:CXER0271
    

connector:log-read

[$connector:log-read] ($connector::element $verb::string $after::string $opts::map {}) -> any — Read a sqlite change table past a position (§3.8.10, sync.md §3.4): a probe of the positions it holds, answered first as [log-position held= baseline= latest= from=], then the walk of its entries cx_seq > from in ordinal order, 500 a page, under the walk's own bounds and stop rule. An empty $after starts just before the newest baseline marker. The position is a BOUND parameter and must be a non-negative decimal (CXER6300). It decides nothing about a gap — that is sync's CXER6413. On backend=postgres the position is an LSN as its decimal: an empty $after reads the BASELINE (the table under one snapshot, ordered by key, at the flushed LSN it saw) and the slot up to that LSN, dropping every transaction the snapshot saw; a position first ADVANCES the slot to it (the cursor sync committed, never further) and then peeks at most 10 000 messages past it. pgoutput is decoded in cx; each change is a [row [cx_seq <commit end LSN>] [cx_op insert|update|delete] …columns], in commit order, after [log-position backend=:postgres held= earliest= latest= baseline= from= next=], or the position alone carrying lost= when the slot is gone, its WAL is lost, or the table was truncated. On backend=mysql the position is the binlog file's sequence × 2^32 + an offset: an empty $after reads the BASELINE in one call (the engine's CX SNAPSHOT: the table READ-locked, the binlog position under the lock, the rows ordered by key at time_zone +00:00); a position is read through the engine's CX BINLOG (the replication protocol, at most 10 000 events); the ROW events are decoded in cx, each change a [row [cx_seq <its transaction's end>] [cx_op …] …columns] in commit order after [log-position backend=:mysql file= held= earliest= latest= baseline= from= next=], or the position alone carrying lost= when the cursor's file is purged or the table was truncated.

      [?lib 'cx-platform/connector']
[$connector:log-read [connector feature='acme' tenant='acme'] 'acme/list-orders' '']
    
      cx-err:CXER0271
    

connector:token

[$connector:token] ($connector::element $gateway::string $opts::map {}) -> element — Mint or re-use this (gateway, tenant)'s machine token. A machine grant has no refresh token by construction — it is re-MINTED — and two tenants of one connector never share one.

      [?lib 'cx-platform/connector']
[$connector:token [connector feature='acme' tenant='acme'] 'acme-api']
    
      cx-err:CXER0271
    

connector:describe

[$connector:describe] ($connector::element $gateway::string $opts::map {}) -> element — Fetch a gateway's schema document and answer a bounded [schema]. Discovery informs validation and the agent-facing description; it NEVER adds a noun, a verb or a field to a running feature.

      [?lib 'cx-platform/connector']
[$connector:describe [connector feature='acme' tenant='acme'] 'acme-api']
    
      cx-err:CXER0271
    

connector:apply

[$connector:apply] ($host::element $feature::string $verb::string $intent::element $opts::map {}) -> any — The act every connector performs, the body a feature package's apply delegates to (§13.2): open the connector (idempotent per deployment, feature and tenant), then an OBSERVE verb walks every page under the one stop rule and answers the pages, or the first err a walk carried and no pages; an act verb is one run. A connector that needs a richer apply writes its own.

connector:observes

[$connector:observes] ($f $feature::string $verb::string) -> bool — Whether $verb (qualified <feature>/<verb> or bare) is an observe verb of the feature document $f — the question apply asks before it walks, public so a connector writing its own apply asks the same one.

connector:listed

[$connector:listed] ($walk) -> any — A walk's pages, or the first err the walk carries — never both, so a clipped list never reads like a whole one.

connector:readout

[$connector:readout] ($host::element $feature::string $t::datetime) -> element — What the connector DID, read off the host journal and never off the vendor: this feature's connector records on the cx:audit stream committed by $t, answered as a [readout] counting calls and calls refused. The body a feature package's readout delegates to (§13.2).

connector:door-delivery

[$connector:door-delivery] ($gateway::element $request::element) -> element — Read a pushed [request] into the [delivery] run admits, by the headers the kind=webhook GATEWAY declares — the id, the sender's instant and the MAC — with the body kept raw and measured in bytes, never decoded (connector.md §3.10.10). The request half of a door, so a host that mounts the route writes none.

      [?lib 'cx-platform/connector']
[?let [= $g [gateway name=hook kind=webhook signing=hmac-sha256-header path='/hooks/orders' method=POST signature-header='x-signature' timestamp-header='x-timestamp' delivery-id='header:x-delivery-id' replay-window='5m']]
      [= $req [request method=POST path='/hooks/orders' [headers [header name='X-Delivery-Id' value='d-1'] [header name='x-signature' value='ab12']] [body '{"type":"order.created"}']]]
      [= $d [$connector:door-delivery $g $req]]
  [probe [id [$string $d@id]] [at [$string $d@at]] [signature [$string $d@signature]] [bytes $d@bytes] [body-raw [= [$string [$first [?for [in $x $d/body] [yield $x]]]] '{"type":"order.created"}']]]]
    
      [probe [id d-1] [at ''] [signature ab12] [bytes 24] [body-raw true]]
    

connector:door-answer

[$connector:door-answer] ($verdict::map) -> element — Write the verdict for one push back to the sender as a [response]: 200 for an admitted or a duplicate delivery, the classification row's status for a refusal, 429 with a delta-seconds Retry-After for a spent sender window, 500 for a refusal no row classifies — the verdict passed as a map of its parts, never the err (connector.md §3.10.10).

      [?lib 'cx-platform/connector']
[?let [= $line [?fn ($r) [answer status=$r@status retry=[$string [$first [?for [in $h $r/headers/header] [where [= [$string $h@name] 'Retry-After']] [yield [$string $h@value]]]]] body=[$string [$first [?for [in $x $r/body] [yield $x]]]]]]]
  ([$line [$connector:door-answer {delivery: 'd-1' duplicate: false}]],
   [$line [$connector:door-answer {delivery: 'd-1' duplicate: true}]],
   [$line [$connector:door-answer {code: 'cx-err:CXER6328' status: 401}]],
   [$line [$connector:door-answer {code: 'cx-err:CXER6307' retry-after: '12s400ms'}]],
   [$line [$connector:door-answer {code: 'cx-err:CXER0001'}]])]
    
      ([answer status=200 retry='' body='{"delivery":"d-1","outcome":"accepted"}'], [answer status=200 retry='' body='{"delivery":"d-1","outcome":"duplicate"}'], [answer status=401 retry='' body='{"code":"cx-err:CXER6328","outcome":"refused"}'], [answer status=429 retry='13' body='{"code":"cx-err:CXER6307","outcome":"throttled"}'], [answer status=500 retry='' body='{"code":"cx-err:CXER0001","outcome":"refused"}'])