Skip to Content

Connector Endpoints

External data source connectors — catalog CRUD, credential custody, read-only probes, and schema discovery (R44a), plus exposure authoring: parameterized, sqlglot-validated, hash-pin-approved SELECT queries with capped preview (R44b). Served by the connector-service (port 8011) and proxied through the API gateway at http://localhost:8000/api/v1/connectors*.

Endpoints

MethodPathPermissionDescription
GET/connectorsexposures.proposeList the tenant’s connectors
POST/connectorsconnectors.manageRegister a connector (201)
GET/connectors/enginesexposures.proposeEngine metadata (capabilities, config JSON schema, credential fields)
GET/connectors/{key}exposures.proposeFetch one connector
PATCH/connectors/{key}connectors.manageUpdate display/config fields (config change → re-probe)
DELETE/connectors/{key}connectors.manageRemove a connector (204; 409 if it has approved exposures)
POST/connectors/{key}/disableconnectors.manageExplicit disable
POST/connectors/{key}/enableconnectors.manageRe-enable — always through a fresh probe
PUT/connectors/{key}/credentialconnectors.manageSet the credential (write-once payload; triggers probe + discovery-on-pass)
POST/connectors/{key}/probeconnectors.manageRe-run the read-only probe
POST/connectors/{key}/discoverconnectors.manageRe-run schema discovery
GET/connectors/{key}/schemaexposures.proposeCurrent schema snapshot
POST/connectors/validateexposures.proposeValidate SQL (shared validator as a service — live UI errors)
GET/connectors/{key}/exposuresexposures.proposeList exposures (?status= filter)
POST/connectors/{key}/exposuresexposures.proposeCreate a draft exposure (201)
GET/connectors/{key}/exposures/{ekey}exposures.proposeFetch one exposure
PATCH/connectors/{key}/exposures/{ekey}exposures.proposeEdit content (reverts to draft, re-hashes)
DELETE/connectors/{key}/exposures/{ekey}exposures.proposeDelete (204; 409 if approved — retire first)
POST/connectors/{key}/exposures/{ekey}/submitexposures.proposedraftpending_review
POST/connectors/{key}/exposures/{ekey}/approveexposures.approveApprove — pins approved_sql_hash
POST/connectors/{key}/exposures/{ekey}/rejectexposures.approvepending_reviewrejected
POST/connectors/{key}/exposures/{ekey}/retireexposures.approveRetire an approved/pending exposure
POST/connectors/{key}/exposures/{ekey}/previewexposures.proposeCapped preview against the live source

Permissions map to roles as: connectors.manage = admin; exposures.propose = admin + power_user; exposures.approve = admin. Operators and reviewers have no connector surface. Enforcement is unconditional — 401/403 regardless of AEGIS_AUTH_MODE.

Three endpoints are service-internal (R44c) — the gateway returns 404 Not Found and they take the tenant from the X-Tenant-Id header only, with 401 for a header-less caller: GET /connectors/{key}/status (registry existence/active/verified check), GET /connectors/{key}/bindings (bulk-describe approved exposures for tool-schema building), and POST /connectors/{key}/exposures/{ekey}/execute (the agent execute path). Only the routes in the table above are client-reachable. POST /connectors/validate and the client-facing POST …/preview are reachable; only these three internal paths are guarded (matched by path segment status/bindings/execute).

Connector status lifecycle: unconfigured → probing → active | probe_failed, plus disabled via explicit disable (enable re-probes, never straight to active).


POST /connectors

Register a connector. The config is validated against the engine’s typed schema, and the destination is egress-validated before any row exists — a private/denied destination is rejected typed and audited.

curl -X POST http://localhost:8000/api/v1/connectors \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "connector_key": "operator_prod_db", "display_name": "Operator Production DB", "description": "Field production and allocation data", "connector_type": "postgres", "config": {"host": "db.operator.example.com", "port": 5432, "database": "production", "sslmode": "verify-full"}, "data_classification": "internal" }'

Per-type config:

EngineFields
postgreshost, port (default 5432), database, sslmode (disable | require | verify-ca | verify-full)
snowflakeaccount (locator, e.g. atvgpmo-nk51631), warehouse, database, schema, role (optional)

Response (201):

{ "connector_key": "operator_prod_db", "display_name": "Operator Production DB", "description": "Field production and allocation data", "connector_type": "postgres", "config": {"host": "db.operator.example.com", "port": 5432, "database": "production", "sslmode": "verify-full"}, "status": "unconfigured", "has_credential": false, "read_only_verified_at": null, "last_probe": null, "data_classification": "internal", "created_at": "2026-07-18T10:00:00Z", "updated_at": "2026-07-18T10:00:00Z" }

Errors422 with a typed detail.error:

ErrorMeaning
config_invalidConfig fails the engine’s schema (per-field messages)
egress_deniedDestination resolves to a denied range (loopback, link-local, RFC1918, CGNAT, …); also audited as connector.egress_denied
resolution_failedHostname did not resolve

409connector_key already exists for the tenant. connector_key must be a lowercase slug ([a-z][a-z0-9_-]{0,63}) — it becomes part of the secret name and later tool names.


GET /connectors/engines

Engine metadata for the register modal and capability display.

curl http://localhost:8000/api/v1/connectors/engines \ -H "Authorization: Bearer $TOKEN"

Response (200): one entry per engine (postgres, snowflake), each with a capabilities map (how the engine enforces read-only, egress pinning, timeouts), the config_schema (JSON Schema for the config object), and credential_fields (e.g. ["password", "username"] for postgres, ["private_key", "username"] for snowflake).


PUT /connectors/{key}/credential

Set the connector’s credential. Write-once semantics: the payload is validated against the engine’s typed credential model, written through to the secret store, and never echoed — validation errors are sanitized to field names only. Setting a credential clears read_only_verified_at and immediately triggers a probe (and discovery on pass).

# postgres curl -X PUT http://localhost:8000/api/v1/connectors/operator_prod_db/credential \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"credential": {"username": "aegis_readonly", "password": "..."}}' # snowflake — key-pair only (PEM PKCS#8 private key; no password auth) curl -X PUT http://localhost:8000/api/v1/connectors/warehouse/credential \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"credential": {"username": "AEGIS_SVC", "private_key": "-----BEGIN PRIVATE KEY-----\n..."}}'

Response (200):

{ "credential": "set", "status": "active", "probe": { "at": "2026-07-18T10:01:12Z", "ok": true, "checks": { "role_attributes": {"ok": true, "detail": "no dangerous role attributes"}, "table_privileges": {"ok": true, "detail": "no write privilege on any relation"}, "schema_create": {"ok": true, "detail": "no CREATE on any schema"}, "database_create": {"ok": true, "detail": "no CREATE on the database"}, "behavioral_create": {"ok": true, "detail": "CREATE TABLE denied"} }, "error": null, "remediation": null } }

Errors: 422 credential_invalid (sanitized field list — the offending value is never included), 409 if the connector is disabled, 503 secret_store_unavailable.


POST /connectors/{key}/probe

Re-run the fail-closed read-only probe. Requires a credential (409 no_credential otherwise).

Response (200): {"status": "...", "probe": {...}} with the same probe shape as above. On failure, probe.error is typed (write_capable, unreachable, resolution_failed, credential_unavailable, probe_error, egress_denied) and probe.remediation contains ready-to-run GRANT/REVOKE SQL for the source DBA.

Postgres checks: transitive role-attribute scan (rolsuper/rolcreatedb/rolcreaterole/rolbypassrls), has_table_privilege write scan, schema/database CREATE scans, rollback-wrapped CREATE TABLE. Snowflake checks (stricter — no session read-only mode exists): transitive SHOW GRANTS walk failing on any write/OWNERSHIP/CREATE* grant or admin-role membership, plus a CREATE TEMPORARY TABLE behavioral check.


POST /connectors/{key}/discover

Re-run schema discovery. Refuses until the probe has passed.

Response (200):

{"schema_count": 1, "table_count": 3, "truncated": false, "truncation_reason": null}

Errors: 409 not_verified (probe has not passed), 422 egress_denied, 502 discovery_failed, 503 credential_unavailable. Oversized sources degrade loudly (truncated: true with a truncation_reason) — schema-count cap 50, snapshot byte cap 2 MB.


GET /connectors/{key}/schema

Return the current schema snapshot (404 if discovery has not run yet).

Response (200):

{ "schemas": [ { "name": "public", "tables": [ { "name": "daily_production", "columns": [ {"name": "well_id", "type": "integer", "nullable": false}, {"name": "prod_date", "type": "date", "nullable": false}, {"name": "oil_bbl", "type": "numeric", "nullable": false} ], "approx_rows": 2700 } ] } ], "truncated": false }

Exposures (R44b)

An exposure is a first-class, connector-scoped, parameterized SELECT query. It is validated structurally (single read statement, no DML/DDL/utility node, no blocklisted function, named :placeholders only) via the shared sqlglot validator, previewed with hard caps, and approved by a hash-pinned admin sign-off. Approve once, bind to many skills (R44c).

Lifecycle: draft → pending_review → approved (or rejected), plus retired. Any edit to content reverts an approved exposure to draft and clears the approval pin — the approval signs the exact sql_text, so laundering an edit past a prior approval is impossible (spec D6).

POST /connectors/validate

The shared validator as a service — powers the authoring editor’s live errors. Structural only (table/column existence is proven by preview, not a stored snapshot). Always returns 200; an invalid draft is a normal UI state.

// request {"sql_text": "UPDATE wells SET x=1", "dialect": "postgres", "params": []} // response {"ok": false, "violations": [{"code": "forbidden_node", "message": "UPDATE is not allowed in an exposure query"}], "param_names": []}

Violation codes: empty, parse_error, multiple_statements, not_read_only, forbidden_node, forbidden_function, unnamed_placeholder, numbered_parameter, undeclared_param.

POST /connectors/{key}/exposures

Create a draft. dialect is derived from the connector engine (a mismatched author-supplied dialect → 422 dialect_mismatch). SQL is validated (422 sql_invalid with a violations list). params each declare {name, type: string|number|boolean|date|timestamp, required, default, description}.

{ "exposure_key": "well_flaring", "display_name": "Well flaring by API", "description": "Total flared mcf for a well — written for the model (R44c tool description)", "sql_text": "SELECT api, flared_mcf FROM wells WHERE api = :api", "params": [{"name": "api", "type": "string", "required": true}], "row_cap": 200, "byte_cap": 262144, "timeout_s": 10 }

The response carries sql_hash (sha256 of the raw exact sql_text), review_status, approved_sql_hash, and is_executable (true only when review_status == "approved" and sql_hash == approved_sql_hash).

POST /connectors/{key}/exposures/{ekey}/approve

Admin-only. Re-validates, then pins approved_sql_hash = sql_hash and records approved_by/approved_at. Optional {"note": "..."}. 409 unless the exposure is pending_review.

POST /connectors/{key}/exposures/{ekey}/preview

Execute a draft/approved exposure against the live source, capped at min(row_cap, 50) rows. Fail-closed: only against a probe-verified connector (active + read_only_verified_at), else 409 not_verified. The first path that runs SQL against a customer DB — it uses the same executor R44c will route agent calls through (validate → coerce params → sqlglot LIMIT-wrap → driver-bound execute → loud row/byte truncation). Params bind driver-native and server-side (postgres $N, snowflake :N numeric); values are never string-interpolated.

// request {"params": {"api": "42-123-45678"}} // response { "status": "truncated", "columns": ["api", "flared_mcf"], "rows": [["42-123-45678", 120.5]], "row_count": 50, "truncated": true, "elapsed_ms": 84, "row_cap_applied": 50, "preview_note": "preview capped at 50 rows — results truncated" }

Errors: 422 param_validation (missing/mistyped/unknown param), 422 egress_denied, 502 typed engine error (query_timeout/query_error/connect_error), 503 credential_unavailable. Every attempt writes a connector_execution_log row with the params hash only — values are never logged.

POST /connectors/{key}/exposures/{ekey}/execute (service-internal, R44c)

The agent execute path — NotFound at the gateway; orchestration calls it directly with X-Tenant-Id/X-User-Id headers (tenant from the header only, never the body). Runs the exposure’s full row_cap through the same executor as preview, with two additions preview skips: it recomputes sql_hash from the current sql_text and refuses on a mismatch with approved_sql_hash (the T7 DB-tamper drill → 409 hash_mismatch + a connector.exposure_hash_mismatch audit event), and it degrades a disabled/unverified connector to 409 connector_unavailable.

// request (context is optional; used for the log row) {"params": {"api_number": "42475100001000"}, "context": {"conversation_id": "conv-1", "skill_key": "operator_production_lookup_sf"}} // response { "status": "ok", "columns": ["api_number", "month", "total_gas_mcf"], "rows": [["42475100001000", "2026-06-01", 41230.5]], "row_count": 1, "truncated": false, "elapsed_ms": 91, "sql_hash": "9f2c…" }

Typed errors surface to the LLM as tool errors (never a fabricated empty result, the A5 rule): 409 connector_unavailable, 409 exposure_not_approved / 409 hash_mismatch, 422 param_validation, 422 egress_denied, 502 engine error, 503 credential_unavailable. A connector_execution_log row is written per call (ok/truncated/error/denied) with the params hash only.

GET /connectors/{key}/status · GET /connectors/{key}/bindings (service-internal, R44c)

GET …/status{connector_key, exists, active, read_only_verified, connector_type} — the registry client’s 30 s-cached check that decides whether a bound tool loads or degrades. GET …/bindings → a list of {exposure_key, display_name, description, params, row_cap, data_classification, is_executable} for the connector’s approved, executable exposures (effective data_classification = the exposure’s own label else the connector floor) — orchestration turns each into an xq_{connector_key}_{exposure_key} tool.


Audit Events

Every mutation lands an HMAC-signed row in the append-only audit_logs table:

ActionNotes
connector.created / updated / deletedCatalog lifecycle
connector.disabled / enabledExplicit state changes
connector.credential_setSHA-256 digests of the payload and username only — never material
connector.probe_passed / probe_failedEvery probe verdict
connector.discoveredSchema/table counts + truncation flag
connector.egress_deniedHost, resolved IP, and deny reason
exposure.proposed / edited / deletedAuthoring lifecycle (carries sql_hash; edited flags reverted_from_approved)
exposure.submitteddraftpending_review
exposure.approvedCarries the pinned approved_sql_hash
exposure.rejected / retiredReview decisions

Exposure previews and (R44c) executions land in connector_execution_log (not audit_logs) — one row per attempt with the params hash, status (preview/ok/truncated/error/denied), row count, elapsed ms, and typed error_class. Param values are never stored.

Credential material never appears in any API response, database row, log line, or audit entry — the catalog stores a secret-store reference only, and the UI only ever sees set/unset.

Last updated on