DataShield Ontology · documentation
Connectors: connection paths, the connection umbrella, and the dialable kinds
*Measured against /home/datashield/library at v0.22.188 (tag 2b5f46a9c, 2026-09-21). Every claim below carries a file:line you can re-read; where a value was obtained by evaluating the config module under tsx rather than by reading a literal, the section says so. Normative keywords per RFC 2119.*
Audience: developers and AI agents connecting a data source to the Library/Ontology. Read §2 (paths), §3 (the connection umbrella) and §5 (your kind's row) before creating anything; read §9 before you trust a probe verdict; read §11 for the two registrations that were scripted on 2026-09-21 and what the ledger says actually ran.
On this page
- 1. Vocabulary
- 2. STANDARD-010 — exactly two connection paths, and who owns what
- 3. The connection umbrella
- 4. Credential shapes — PLATFORM_CREDENTIAL_FIELDS and BEARER_CREDENTIAL_SHAPE
- 5. Per-kind capability table (measured)
- 6. Declared-only kinds — stated honestly
- 7. Path-B specifics: metadata-only allowlists, budgets, ceilings
- 8. Guards every dial passes through
- 9. Dial verdicts — the classifier and the typed refusals
- 10. Governance evidence harvested per kind
- 11. Worked example — registering Salesforce and MySQL (2026-09-21)
- 12. Adding a kind — the checklist the config enforces
1. Vocabulary
| Term | Meaning | Where it is defined |
|---|---|---|
| connection | An encrypted credential row (registry.connections), AES-256-GCM at rest, owned by the caller's account, optionally shared | scripts/mcp-server/umbrellas/connection.ts:54-57 |
connection_type | The auth shape of the stored config: oauth2 | api_key | basic | bearer | custom | connection.ts:92 |
platform | Which provider the credential dials — a closed vocabulary equal to the connection-backed PROVIDER_KINDS ids (z.enum(CONNECTION_BACKED_KIND_IDS)); orthogonal to connection_type | connection.ts:101 |
| provider kind | A PROVIDER_KINDS row in config/data-catalog.config.ts:483 with a path (A | B | ingest), a harvest transport and lane flags | config/data-catalog.config.ts:483, :734 |
| probe kind | The wire protocol a liveness verdict rests on (postgres_dial, tds_dial, saas_get, …) | config/connection.config.ts:771 |
| lane | One of discover | profile | extract (LANE_CAPABILITIES) run by data_catalog.scan_run over a provider | config/data-catalog.config.ts:1967 |
A connection is not a provider. The sequence is always: connection.create (credential) → data_catalog.provider_create (bind the credential to a kind) → data_catalog.provider_test / connection.test (liveness) → data_catalog.scan_run (lanes).
2. STANDARD-010 — exactly two connection paths, and who owns what
Source: docs/standards/STANDARD-010-connection-paths-and-ownership.md (operator ruling 2026-09-15, amended for the catalog-elt-1 wave).
| Path | For | Mechanism (STANDARD-010 §1) | Kinds at v0.22.188 (measured, §5 below) |
|---|---|---|---|
| A — native dial | databases and object stores that matter to customers | vendor SDK loaded at dial time (NO_LOADTIME_RESOLUTION), DIALERS material table, one dialability predicate, dialect-keyed read-only SQL guard, metadata-only harvest, typed dial verdicts | postgresql, snowflake, databricks, bigquery, sqlserver, mysql, s3 (7) |
| B — REST + vault | every SaaS / API-shaped source | saas_api (or rest_api) transport, credentials in the Ontology vault, one OAuth2 acquirer, one SaaS metadata-catalog harvester contract | salesforce, dynamics365, rest_api, iceberg_rest, kafka (5 — the last two are declared only, §6) |
ingest (not a path) | pre-existing file/feed ingestion lanes | never dials a provider from the catalog; a kind that gains a dial moves to A or B | azure_blob, gcs, sftp, ftp, remote_storage, imap, socrata, tyler, ckan, arcgis, opendatasoft, dcat, filesystem, other (14) |
Rules that bind a connector author (STANDARD-010 §1, §5):
- A third path (ODBC/JDBC driver sets, generic sockets, bespoke transports) MUST be refused at groom. The correct move is a
new Path-A thin client or a Path-B kind. Native binaries only when no thin client exists AND the operator rules.
- Gate
audit:catalog-config-agreementleg T30: everyPROVIDER_KINDSrow declarespath+pathWhy; a row with a
driverModule MUST be A; a saas_api/rest_api transport MUST be B.
- Path B kinds MAY dial. Dialability is answered by the transport's
objectSurface
(HARVEST_TRANSPORT_CAPABILITIES, config/data-catalog.config.ts:656: source_pool → engine_catalog, saas_api → metadata_api, both dials: true; local_projection → platform_registry, dials: false), not by the letter.
Ownership (STANDARD-010 §2). Ontology (this Library) is the loaded home of every data connection — all Path-A and Path-B kinds, their credential shapes, dial probes, lanes and vault entries. Auth is the exclusive home of security connectors (Entra ID, Okta, logging/security tooling, signing keys, MCP service registrations) and the passthrough gateway. Both MUST expose the full connector list read-only. Until the shared @datashield/connection-contracts package exists (§3), audit:auth-connection-type-parity keeps the two registries honest; a kind Auth does not carry declares library_only with a reason (e.g. BEARER_CREDENTIAL_SHAPE.authParity, config/data-catalog.config.ts:1730). Commander and third-party MCP clients are consumers through Auth entitlements, never a third runtime (§6).
3. The connection umbrella
File: scripts/mcp-server/umbrellas/connection.ts (623 lines). Description text at :54-86; Zod command union at :88-122; handler cases at :183 (create), :229 (list), :264 (get), :288 (delete), :297 (test).
| Command | Params (from the Zod schema) | Behaviour measured in the handler |
|---|---|---|
create | name, connection_type (oauth2|api_key|basic|bearer|custom), config (object), optional description, provider_name, base_url, scopes, tags, expires_at, platform; is_shared admin-only | Owned by the caller. platform is validated against registry.provider_kinds WHERE connection_backed via assertKindUsable (:202-210, LIB-BUG-1350 K1) |
list | filters provider_name, connection_type, tag; paginated | Metadata only — never returns config. Returns platform and the latest probe outcome (:246-259) |
get | id | Same metadata shape as list (:273-282) |
test | id | Probe chosen by the row's platform through PLATFORM_PROBES (:354); null platform → legacy classification (:348-350) |
delete | id, confirm: true (literal) | Soft-delete — deactivates the row, owner/admin only (:288-296, :117-121) |
Config is write-only. create stores it encrypted; list/get return metadata only and the credential can never be read back (connection.ts:55). The latest probe outcome is deliberately readable (LIB-BUG-556 #2, :250, :278).
connection_type vs platform. connection_type says how the secret is shaped for auth; platform says what it dials and therefore which probe and which data_catalog kind can bind it (:101, :85). A row created without platform is "unplatformed"; data_catalog.provider_create can stamp it later. Note the 2026-09-21 fresh-client prompt (docs/audit/v0.22.188-fresh-client-test-prompt.md:125) passes connection_type:'mysql', which is not in the CONNECTION_TYPES enum — the prompt itself says "if connection_type does not accept mysql, that disagreement is the finding" (:129-130). Use connection_type:'basic' (or the shape your kind's material declares) and platform:'mysql'.
3.1 Probe kinds per platform — PLATFORM_PROBES
config/connection.config.ts:1011-1103 (kind vocabulary CONNECTION_PROBE_KINDS at :771; resolver-dial subset at :963).
platform | probe kind | What the probe does |
|---|---|---|
postgresql | postgres_dial | opens a real session; SELECT current_setting('server_version') (PLATFORM_DIAL_PROBES, config/data-catalog.config.ts:2110) |
sqlserver | tds_dial | real session; SELECT @@VERSION |
snowflake | snowflake_dial | real session; SELECT CURRENT_VERSION() |
mysql | mysql_dial | real session; SELECT VERSION() |
bigquery | bigquery_dial | sql_dry_run of SELECT 1 |
databricks | databricks_dial | SELECT current_version().dbsql_version |
dynamics365, salesforce | saas_get | ONE kind for both: a credentialed HTTPS GET of a declared metadata path — /WhoAmI and /limits respectively (connection.config.ts:1065-1069, LIB-BUG-1433) |
rest_api | http_head | SSRF-guarded HEAD of base_url, no credential (connection.ts:73) |
s3, azure_blob, gcs, sftp, ftp, remote_storage, imap | storage_dial | provider's own testConnection(); for s3 the dial probe is object_store_list with maxKeys: 1 |
iceberg_rest, kafka | not_applicable | typed test_not_applicable naming the platform — never a fabricated unreachable (connection.ts:70, 366-370) |
The probe kinds are also rows of registry.probe_kinds (migration 0239 FK), seeded from this tuple as an unconditional post-step of release:migrate — which is why release:migrate MUST run before any pdl-mcp/pdl-job-worker reload (connection.config.ts:1054-1061; confirmed at the v0.22.188 event, docs/audit/v0.22.188-release-event.md:23). Probe timeout: CONNECTION_TEST_TIMEOUT_MS = 10_000 (connection.config.ts:448).
4. Credential shapes — PLATFORM_CREDENTIAL_FIELDS and BEARER_CREDENTIAL_SHAPE
config/data-catalog.config.ts:1253 (evaluated under tsx; * = required, (s) = secret). These are the members of the config object you pass to connection.create.
| kind | fields |
|---|---|
postgresql | host*, port, database*, username*, password*(s), sslmode |
mysql | host*, port, database*, username*, password*(s), ssl |
sqlserver | host*, port, database*, username*, password*(s), encrypt, trustServerCertificate |
snowflake | account*, username*, password(s) or privateKey(s) + privateKeyPassphrase(s), warehouse, database, schema, role |
bigquery | project_id*, client_email*, private_key*(s), location |
databricks | server_hostname*, http_path*, access_token*(s), catalog, schema |
s3 | bucket*, region, endpoint, access_key_id(s), secret_access_key(s), force_path_style |
salesforce | instance_url*, client_id*, client_secret*(s), token_url (derived from the my-domain host — omit), scope (never invent) |
dynamics365 | org_url*, tenant_id*, client_id*, client_secret*(s), token_url, scope |
BEARER_CREDENTIAL_SHAPE (data-catalog.config.ts:1730) is the material both Path-B kinds claim (PLATFORM_MATERIAL_KIND.salesforce = "bearer"): token_url*, client_id*, client_secret*(s), scope, audience. A module-load check (:1794) refuses a platform whose credential fields omit a field its material requires. Its authParity is library_only: Auth's registry declares no client-credentials grant shape, so no parity is claimed (:1744-1769).
S3-compatible endpoints (S3_COMPATIBLE_ENDPOINTS, connection.config.ts:104) and server-side-encryption decisions (decideServerSideEncryption, :368) are declared as data; resolveS3Endpoint (:259) picks the endpoint from the provider name.
5. Per-kind capability table (measured)
runnableLanes(kind) and KIND_HARVEST_TRANSPORT (config/data-catalog.config.ts:2028, :734) evaluated under tsx at v0.22.188. kindCanRun (:1915) derives these from each kind's implemented flags — no hand-maintained list exists.
| kind | path | transport | driver (driverModule) | discover | profile | extract | credential read-only probe |
|---|---|---|---|---|---|---|---|
postgresql | A | source_pool | pg | yes | yes | yes | yes — SQL over information_schema.role_table_grants (CREDENTIAL_READONLY_PROBES, :3914) |
snowflake | A | source_pool | snowflake-sdk | yes | yes | yes | null (declared) |
sqlserver | A | source_pool | mssql | yes | yes | yes | null |
mysql | A | source_pool | mysql2 (:349; pinned exactly at 3.24.4 in package.json:383) | yes | yes | yes | null |
bigquery | A | source_pool | @google-cloud/bigquery | yes | yes | yes | null |
databricks | A | source_pool | @databricks/sql | yes | yes | yes | null |
s3 | A | source_pool | @aws-sdk/client-s3 | yes | no | no | null |
salesforce | B | saas_api | — | yes | no | no | null (declared, v0.22.187-task-elt1-b2-salesforce.md §1 row 8) |
dynamics365 | B | saas_api | — | yes | no | no | null |
rest_api | B | local_projection | — | yes | no | yes (scoped assets) | — |
What the lanes mean:
- discover — harvests the provider's catalog (schemas, tables/objects, columns, types, keys) into
catalog.assets/
catalog.bindings. For source_pool kinds this reads the engine's own information_schema-class views through a pooled session; for saas_api kinds it walks the metadata API only (§7).
- profile — per-column statistics and (where the sample policy permits) samples. Governed by
applySamplePolicyand
the acknowledgement rungs in lib/data_catalog/policy.ts:114, :373.
- extract — a FULL extract per binding (M3) into a Library dataset (
lib/data_catalog/extract/handler.ts:6). See §8.
6. Declared-only kinds — stated honestly
iceberg_rest and kafka are PROVIDER_KINDS rows with path: "B", transport source_pool, **no driverModule, no credential shape, no dial probe and no runnable lane** (runnableLanes → []). connection.test returns test_not_applicable for them by name (connection.config.ts:1101-1102; connection.ts:70). LIB-BUG-1043 was narrowed to exactly mysql, iceberg_rest, kafka at the v0.22.188 event (v0.22.188-release-event.md:170); mysql then shipped in the same release, so the residue is these two. azure_blob, gcs, sftp, ftp, remote_storage, imap are ingest kinds: they have a storage_dial liveness probe through the legacy provider registry but no catalog lanes (runnableLanes → []); the open-data portal kinds and filesystem/other have neither transport nor lanes. A data_catalog.provider_create over any of these is a typed refusal, not a silent empty scan.
7. Path-B specifics: metadata-only allowlists, budgets, ceilings
7.1 Salesforce — config/salesforce.config.ts
- API version
67.0(:64), prefix/services/data/v67.0(:67). - Exact-shape allowlist, not a prefix (
:25-35,:174-192): the admitted shapes aredescribe_global(/sobjects),
sobject_describe (/sobjects/{Name}/describe) and limits (/limits — the liveness path of the bullet below). The reason is load-bearing: /sobjects/Account/001xx… is a customer record and /sobjects/Account/listviews names filter values; a prefix rule admits both. assertMetadataOnlyPath() (:561) refuses anything else and names the segment at which the path left the allowlist (:199-202).
- The object name interpolated into the next path is org-controlled input and is held to
SALESFORCE_OBJECT_NAME.pattern
(:72-89); unnormalised path markers are refused (:124).
- Liveness:
/limits— requires a session and reads no record;/services/data/answers 200 unauthenticated and would
report a credential healthy without presenting one (SALESFORCE_LIVENESS, :228-235). reportsServerVersion: false.
- Object filter: skips
queryable: falseandretrieveable: falsesObjects (:286-290). Measured on the target org
2026-09-16: 1700 sObjects, 1433 queryable, 2 custom (:270-271).
SALESFORCE_BUDGET = { maxDescribesPerScan: 1000, describeConcurrency: 1 }(:335-350;describeConcurrencyat:349). One describe per admitted
object against a 15 000-call daily allocation shared with the customer's other integrations; 1000 ≈ 6.7 % of a day (:310-330). scope.max_entities_per_scan on the crawler outranks it; SAAS_API.paging.maxEntities bounds both.
- Governance evidence (
SALESFORCE_GOVERNANCE,:427-435):field_encryption_declaredwritten for every field;
evidenceStrength: "provider_declared", scoreOnly: true. It names enrolment in Shield Platform Encryption, not an effective read restriction — a reader MUST NOT render it as "masked".
7.2 Dynamics 365 (Dataverse) — config/dynamics365.config.ts
- API
v9.2, prefix/api/data/v9.2(:41-44). - Prefix allowlist (
DYNAMICS365_ADMITTED_PATH_PREFIXES,:159):/EntityDefinitions,/RelationshipDefinitions,
/GlobalOptionSetDefinitions, /WhoAmI. A prefix suffices here because Dataverse metadata collections are disjoint from record collections by URL (:20-23); traversal probes (/EntityDefinitions/../accounts, %2e%2e) are refused at module load (:545). Refusal code path_not_in_metadata_allowlist (:445) — the sentence is value-free by construction.
$selectprojections (DYNAMICS365_SELECT,:186), entity filterIsPrivate eq false(:237), page-size header
odata.maxpagesize, per-request timeout PT60S (ISO 8601, :364), credential statuses [401, 403] (:380).
- Governance evidence (
DYNAMICS365_GOVERNANCE,:352):field_level_security_declaredfromIsSecured;
provider_declared, scoreOnly — enrolment in field-level security, not an effective restriction.
7.3 The shared SaaS contract and the entity-ceiling ladder
config/saas-api.config.ts SAAS_API (evaluated): paging = { defaultPageSize: 200, maxPages: 250, maxEntities: 5000 }; `rateLimit = { maxAttempts: 4, baseBackoff: PT1S, maxBackoff: PT30S, jitterRatio: 0.2, maxRetryAfter: PT2M, retryStatuses: [429, 503], credentialStatuses: [400, 401, 403], transientStatuses: [500, 502, 504, 408, 425] }`. RFC 9110 §10.2.3 Retry-After is honoured in both syntaxes; a credential-fact status is never retried (HARVEST_TRANSPORT_CAPABILITIES.saas_api.rateLimitPosture, data-catalog.config.ts:687-691).
The ceiling ladder (SAAS_ENTITY_CEILING, lib/data_catalog/harvest/saas.ts:211-246, resolved at :249-266): rung 1 the crawler's own scope.max_entities_per_scan; rung 2 the kind's declared per-scan ceiling — SAAS_KIND_ENTITY_CEILING_DEFAULTS (data-catalog.config.ts:726): salesforce: 1000 (= SALESFORCE_BUDGET.maxDescribesPerScan), dynamics365: null; rung 3 the contract's maxEntities: 5000. The driver enforces the winner and records which rung supplied it, so a partial scan is diagnosable (salesforce_objects_truncated, dynamics365_relationships_truncated).
8. Guards every dial passes through
| Guard | Where | What it does |
|---|---|---|
| Read-only SQL guard | config/sql-readonly-guard.config.ts — SQL_GUARD_DIALECT_IDS = ["snowflake","sqlserver","bigquery","databricks","mysql"] (:56), SQL_GUARD_DIALECTS (:275), terminalVerb (:1024), hasStatementSeparator (:951) | Dialect-keyed lexer that strips preambles/comments and refuses any statement whose terminal verb is not a read, or that carries a second statement. Postgres is covered by the credential read-only probe in §5 instead |
| Host policy | HOST_POLICY, config/data-catalog.config.ts:2731 (evaluated): blockMetadataRanges: true, blockSelfDatabase: true, allowPrivateRanges: false (per-connection key allow_private_ranges), allowLoopback: false (env PDL_CATALOG_ALLOW_LOOPBACK), allowUnixSockets: false, dsnQueryAllowlist: [sslmode, application_name, connect_timeout] | Refuses cloud-metadata ranges, the Library's own database, private ranges, loopback and unix sockets before any socket opens |
| Pinned dial | config/host-pinning.config.ts HOST_PINNING (:61) declares a rule for every dialable kind, in two strategies: socket_address for mysql (:66) and sqlserver (:75), and recheck for postgresql (:81), snowflake (:90), s3 (:97), bigquery (:104) and databricks (:128); saas_api/dynamics365/salesforce share SAAS_API_HOST_PINNING (:138-159), socket_address via a DNS-pinned undici Agent (:54-55) | On a socket_address row the address that passed policy is the address dialled — no DNS rebinding between check and connect. A recheck row does not close that window and says so in its own residual field ("the window is narrowed to the recheck-to-connect interval, not closed"): those drivers take a host NAME and resolve it themselves |
| Cleartext refusal | SAAS_CLEARTEXT_REFUSAL_CODE = "cleartext_refused" (config/saas-api.config.ts:261); raised in lib/data_catalog/resolver/index.ts:430 (LIB-BUG-1435) | An http:// token or instance URL is refused before a bearer secret is sent |
| Sample policy | lib/data_catalog/policy.ts:114, :373, :530 | Values leave the customer's engine only under an acknowledged rung |
8.1 The extract lane on the shared loop
EXTRACT_LANE_UNIT (data-catalog.config.ts:2939, evaluated): source_pool → "bindings", `local_projection → "scoped_assets", saas_api → "scoped_assets". lib/data_catalog/extract/handler.ts:133` reads the unit from the transport (close-fix L3: no silent literal, :125); for bindings it iterates catalog.bindings joined to assets (:171), stops at HARVEST_BUDGETS.maxExtractedObjectsPerScan (:190), skips a binding with no dataset (:194) and runs FULL_EXTRACT_PROFILE.mode (:216). Row egress goes through lib/data_catalog/extract/sharedLoop.ts; MySQL became the fifth engine on it with "one spelling row, one KIND_EXTRACT row, one EXTRACTORS slot" and zero loop changes (docs/audit/v0.22.188-task-elt2-a-mysql.md:21).
9. Dial verdicts — the classifier and the typed refusals
classifyDialError (config/connection.config.ts:707) maps err.code / err.name / err.cause.code (undici wraps the socket error) to a registered wire code, first match wins, fallback unreachable (:450-467). Messages are fixed author templates; raw upstream text is logged server-side only.
| code | retryable | retry_after_ms (DIAL_RETRY_AFTER_MS, :681) | class |
|---|---|---|---|
dns_failure | yes | 30 000 | unreachable |
connect_timeout | yes | 60 000 | timeout |
unreachable | yes | 30 000 | unreachable (the honest generic) |
credential_rejected | no | — | auth |
config_rejected | no | — | config |
response_malformed | no | — | — |
dial_failed_unclassified | no | — | unclassified |
A module-load self-check refuses a retryable class without a positive budget, or a budget on a non-retryable class (:1105-1122).
Refusals raised before or around a dial are not dial verdicts and are re-thrown, not flattened (G1 close-vet F-V1, :850-870): host_form_refused (resolver/index.ts:523), the host-policy family (loopback / private range / metadata / self-dial / unix socket), credential_shape_invalid, credential_expired, driver_unavailable, cleartext_refused, and the SaaS token family token_acquire_failed, token_endpoint_unreachable, token_response_too_large (resolver/saasToken.ts:103-140). The probe result then carries a probe_not_run reason selected by phase (CONNECTION_PROBE_NOT_RUN_REASONS, :829: host_unresolvable, resolve_refused, and the third row for refusals raised after preparation succeeded) so the caller is never told "the host did not resolve" about a host that was never queried.
Reading a verdict: ok:false + a code from the table = the provider was reached or attempted; test_not_applicable = the platform has no probe (§6); probe_kind: 'no_declared_probe' = a configuration gap in PLATFORM_PROBES, not a statement about the credential (connection.ts:355-362).
10. Governance evidence harvested per kind
The classification kernel (config/classification-evidence.config.ts) admits provider governance facts as the provider_declared signal: role score (:420), weight 0.10 (:438), strictly below fingerprint (:1259-1262), evidence: "external_claim", mayCorroborate: false (:490) — a provider tag is a customer-authored claim about the column, never an observation of it, so it can raise a score but never gate, veto or corroborate.
| kind | fact harvested | key | source |
|---|---|---|---|
salesforce | Shield Platform Encryption enrolment per field | field_encryption_declared | config/salesforce.config.ts:427-435 |
dynamics365 | field-level-security enrolment (IsSecured) | field_level_security_declared | config/dynamics365.config.ts:352 |
mysql | governance absence declared explicitly | see config/mysql-harvest.config.ts ("governance-ABSENCE declarations", v0.22.188-task-elt2-a-mysql.md:31) | |
bigquery, postgresql, sqlserver, snowflake, databricks | engine-catalog facts (keys, nullability, native type family via NATIVE_TYPE_FAMILY_RULES) | — | per-kind harvest/*.ts |
STANDARD-009 applies to every artefact about the data: value-free, unclassified ≠ public, masking OFF by default.
11. Worked example — registering Salesforce and MySQL (2026-09-21)
What the ledgers record, verbatim in disposition: the v0.22.188 event deployed both kinds (docs/audit/v0.22.188-release-event.md:55-74: mysql.path = A, mysql lanes {discover, profile, extract} all true, salesforce {discover:true, profile:false, extract:false}, four new probe kinds in registry.probe_kinds and in the served bundle, mysql2@3.24.4 installed additively in the primary inside the lock, node_modules 839 → 847). **Deviation 12 (:239-240): "No post-deploy live acceptance was run"** — the MySQL fixture pass and the Salesforce rescan were left to the orchestrator. The registrations below are therefore the scripted acceptance (docs/audit/v0.22.187-task-elt1-b2-salesforce.md §6, :314-349; docs/audit/v0.22.188-fresh-client-test-prompt.md:121-133), not a recorded production run. Do not cite this section as proof a customer org was catalogued.
11.1 Salesforce (Path B, saas_get, discover only)
Prerequisite in the org: an External Client App with the client-credentials flow enabled and a run-as user; without the run-as user the grant fails and provider_test answers provider_dial_auth_failed, which is the correct and diagnosable answer (§6 step 1).
// 1. credential — token_url and scope OMITTED (derived / never invented)
connection {command:'create', name:'acme-salesforce', connection_type:'oauth2', platform:'salesforce',
config:{instance_url:'https://acme.my.salesforce.com', client_id:'<consumer key>', client_secret:'<consumer secret>'}}
// 2. provider over that connection
data_catalog {command:'provider_create', kind:'salesforce', connection_id:'<id from step 1>', name:'Acme CRM'}
// 3. liveness
data_catalog {command:'provider_test', provider_id:'<pid>'}
// expect ok:true, probe_kind:'saas_get', server_version:null (an org reports none)
// 4. discover
data_catalog {command:'scan_run', provider_id:'<pid>', purpose:'discover'}
Expected discover outcome (§6 step 4): saas_object assets > 0 with salesforce_objects_seen ≈ 1700 and salesforce_objects_filtered as denominators; salesforce_api_usage_used / _allocation (≈ 15 000) present; fk on reference columns and field_encryption_declared on every column; salesforce_objects_truncated: 1 means the describe ceiling (§7.1, now 1000) bound a partial, resumable pass. The first check to run: grep the whole result for any 15/18-character Salesforce id outside a key_prefix attr — there must be none. Then read the job-worker log for the tick: no durable-tick DEGRADED, no assertHostPolicy refusal on the my-domain host (§6 step 6).
11.2 MySQL (Path A, mysql_dial, discover + profile + extract)
connection {command:'create', name:'acme-mysql', connection_type:'basic', platform:'mysql',
config:{host:'db.acme.example', port:3306, database:'sales', username:'catalog_ro', password:'<secret>', ssl:true}}
connection {command:'test', id:'<id>'}
// expect probe_kind:'mysql_dial'; a rejected credential → credential_rejected with driver_code + stage
data_catalog {command:'provider_create', kind:'mysql', connection_id:'<id>', name:'Acme Sales DB'}
data_catalog {command:'provider_test', provider_id:'<pid>'} // SELECT VERSION() over a pinned, policy-checked session
data_catalog {command:'scan_run', provider_id:'<pid>', purpose:'discover'}
data_catalog {command:'scan_run', provider_id:'<pid>', purpose:'profile'}
data_catalog {command:'scan_run', provider_id:'<pid>', purpose:'extract'} // bindings unit on the shared loop, §8.1
The fresh-client prompt's local fixture (127.0.0.1:13306) will be refused by HOST_POLICY.allowLoopback: false unless PDL_CATALOG_ALLOW_LOOPBACK is set — that refusal is resolve_refused, not a dial verdict (§9). Every session statement runs through SQL_GUARD_DIALECTS.mysql (§8), and the four pinned session statements plus TLS policy live in config/mysql-session.config.ts.
12. Adding a kind — the checklist the config enforces
Module-load validators in config/data-catalog.config.ts (:4253-4416) and connectionProbeConfigProblems (connection.config.ts:975) refuse a process whose kind is half-declared. A new dialable kind needs, at minimum: a PROVIDER_KINDS row with path/pathWhy (T30); KIND_HARVEST_TRANSPORT; PLATFORM_CREDENTIAL_FIELDS; PLATFORM_MATERIAL_KIND; PLATFORM_DIAL_PROBES; PLATFORM_PROBES + a RESOLVER_DIAL_PROBE_KINDS member if it dials through the resolver; HOST_PINNING; CREDENTIAL_READONLY_PROBES (a declared null with a reason is valid); AUTH_CONNECTION_TYPE_PARITY; a DIALERS factory and HARVESTERS entry; for Path A a SQL_GUARD_DIALECTS row and an exact-pinned driver; for Path B an allowlist config in the shape of §7. No migration is required for the probe kind — the seeder fills registry.probe_kinds from config (connection.config.ts:1054-1056).
Measured against the release named in the text. Raw Markdown: /ontology/documentation/connectors.md. This page is also served as raw Markdown at the same URL with a .md suffix, for agents and search tools that prefer plain text. Every capability statement cites the source file and line it was measured from.
You've seen the proof
Ready for a number? Scope your deployment and we'll price it against your own economics.
Get your quote →