> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getcanonic.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Error codes

> The canonical error registry and its headless exit-code mapping.

Every canonic error carries a stable, wire-safe `code` string plus a headless process exit code, never free text alone, so a caller (script, CI job, agent) can act on it programmatically. The registry lives in `canonic/exc.py` and is guarded by conformance tests, so this mapping can't silently drift.

| Exit code | `code`                          | Meaning                                                                                                                                                                                               |
| --------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2         | `unresolved`                    | A metric name matches no active canonical binding.                                                                                                                                                    |
| 3         | `ambiguous`                     | A name matches more than one active binding. Candidates are returned.                                                                                                                                 |
| 4         | `unreachable`                   | A dimension/filter has no declared join path to the metric's source.                                                                                                                                  |
| 5         | `ambiguous_join_path`           | More than one valid join path exists. The query must name an explicit `via`.                                                                                                                          |
| 6         | `unsupported_measure`           | A non-additive/semi-additive measure was requested at a grain no strategy can safely serve.                                                                                                           |
| 7         | `fanout_unsafe`                 | A join would corrupt a non-additive measure.                                                                                                                                                          |
| 8         | `guardrail_block`               | A `severity: error` guardrail blocked the query.                                                                                                                                                      |
| 9         | `validation_failed`             | A semantic/contract/knowledge file failed validation.                                                                                                                                                 |
| 10        | `assertion_failed`              | An assertion diverged from its expected value beyond tolerance (also used by `canonic assert`'s `--min-accuracy` gate).                                                                               |
| 11        | `read_only_violation`           | A non-`SELECT` statement was submitted to a read-only execution path.                                                                                                                                 |
| 12        | `schema_mismatch`               | Declared schema doesn't match the live source during probe validation.                                                                                                                                |
| 13        | `connection_error`              | A connector couldn't establish or maintain a connection (also raised for an unregistered connector `type`, or an out-of-range connector version).                                                     |
| 14        | `contradiction`                 | Headless `--strict` ingest found a run with any flagged contradiction.                                                                                                                                |
| 15        | `generation_failed`             | An LLM generation call failed (deterministic provider/transport error).                                                                                                                               |
| 16        | `structured_output_invalid`     | The model's output doesn't satisfy the requested JSON schema.                                                                                                                                         |
| 17        | `structured_output_unsupported` | The model/endpoint can't honor schema-constrained output at all.                                                                                                                                      |
| 18        | `air_gapped_violation`          | Air-gapped mode would let context leave the machine (public endpoint, remote secret ref, or telemetry).                                                                                               |
| 19        | `retries_exhausted`             | A transient provider/transport failure persisted past the bounded retry budget.                                                                                                                       |
| 20        | `telemetry_not_configured`      | A real telemetry send was attempted without `enabled`/`endpoint`/`transport_acknowledged` all set.                                                                                                    |
| 21        | `telemetry_send_failed`         | The telemetry HTTP call itself failed (non-2xx, connection error, timeout).                                                                                                                           |
| 22        | `tenant_unresolved`             | A [tenancy policy](/concepts/tenancy-and-access-control) is active but the request carries no resolvable tenant. Raised at compiler stage 0, before metric resolution.                                |
| 23        | `tenant_scope_missing`          | A query reaches a source declared in neither `scoped_sources` nor `shared_sources` of the tenancy policy: a policy hole, not an unfiltered pass-through.                                              |
| 24        | `tenant_forbidden`              | The caller's own policy denies something the caller can already see: `run_sql` denied by role, or `run_sql` refused because the connection lacks `rls_enforced: true` under an active tenancy policy. |

## Resolving `ambiguous`

When a name matches more than one active binding, the error carries a `candidates` list: the exact, unambiguous values to re-issue the request with. Both `--json` and plain-text CLI output, and the MCP tool error payload, include it.

**Ambiguous dimension**: a dimension name is declared on more than one join-reachable source (e.g. `country` exists on both `customers` and a twice-joined `locations`, reached as `pickup`/`dropoff`):

```bash theme={null}
canonic query --metrics rental_revenue --dimensions country
```

```text theme={null}
error ambiguous: dimension 'country' is present on multiple join-reachable sources; qualify explicitly
  candidate 1: customers.country
  candidate 2: dropoff.country
  candidate 3: locations.country
  candidate 4: pickup.country
  hint: qualify with one of the candidates above, e.g. --dimensions customers.country
```

Qualify with `alias.dimension`: the alias is the join's `name` (or the source name for an unnamed join). To pick the one you mean:

```bash theme={null}
canonic query --metrics rental_revenue --dimensions customers.country
```

**Ambiguous metric name**: two active bindings share a name/alias. The `candidates` list names each competing metric. Re-issue with the specific canonical name (or fix the duplicate alias in `contracts/metrics/`) rather than the ambiguous shared one.

## Resolving `tenant_unresolved`

A [tenancy policy](/concepts/tenancy-and-access-control) is loaded, but the request carries no resolvable tenant, most commonly a `stdio` MCP session with no `--tenant` flag, or a bearer token whose claims don't carry the tenancy policy's `claim`:

```bash theme={null}
canonic query --metrics revenue
```

```text theme={null}
error tenant_unresolved: tenancy policy is active but the request carries no resolvable tenant
```

Fix it with `--tenant` for local development (always warns, see [`--tenant` CLI override](/concepts/tenancy-and-access-control#--tenant-cli-override)), or by confirming the token actually carries the configured `claim`:

```bash theme={null}
canonic query --metrics revenue --tenant 4711
```

Setting `on_missing_principal: allow_unscoped` in `tenancy.yaml` avoids the error entirely and serves unscoped with a warning instead, a dev-only escape hatch, not a production setting.

## Resolving `tenant_scope_missing`

A query's join plan reaches a source declared in neither `scoped_sources` nor `shared_sources` of the tenancy policy. This is a policy hole the compiler refuses to serve unfiltered, not a bug to silently work around:

```text theme={null}
error tenant_scope_missing: source 'shipments' is declared in neither scoped_sources nor shared_sources of the tenancy policy
```

Fix it by classifying `shipments` in `tenancy.yaml`: as `scoped_sources` (add its tenant column) if it carries per-tenant rows, or `shared_sources` if it's genuinely tenant-neutral (a lookup/dimension table). Setting `undeclared_source: warn` turns this into a warning instead of a hard failure, useful only for incremental adoption on an existing project. It still means the source is served unfiltered in the meantime.

## Resolving `tenant_forbidden`

Raised only by the [`run_sql` gate](/concepts/tenancy-and-access-control#the-run_sql-gate), for one of two independent reasons:

```text theme={null}
error tenant_forbidden: role denies raw SQL execution (run_sql: false)
```

The caller's role doesn't grant `run_sql: true`. No connection-level fix resolves this: the role itself has to change.

```text theme={null}
error tenant_forbidden: run_sql is refused on connection 'warehouse_pg': a tenancy policy is active and this connection carries no rls_enforced: true attestation
```

Tenancy is active, the caller isn't `tenancy_exempt`, and the target connection has no `rls_enforced: true` in `canonic.yaml`. Fix it by setting `rls_enforced: true`, but only once the warehouse actually enforces the tenant boundary out of band (per-tenant credentials or native row-level security). Setting the flag without the enforcement behind it defeats the point of the gate.

## Errors without a registry code

A few internal/caller-contract errors carry no wire `ErrorCode` and use the default exit code **1**: `CapabilityNotSupportedError` (a connector was asked to honor a capability it doesn't declare), `EmbeddingUnavailable` (embed called without gating on `is_available()`), `CredentialError`, `SemanticSourceError`, `ContractError`, and `KnowledgePageError`. These signal a caller/config mistake rather than a documented, structured failure mode.

<Note>
  `EvalDatasetError` (a malformed `canonic eval baseline` dataset/candidates file) and `KnowledgeReferenceError` (a broken `sl_ref`/page link) both reuse `validation_failed` (exit 9) rather than defining new codes. They're the same class of failure as any other invalid input file.
</Note>
