Skip to main content
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.

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):
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:
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 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:
Fix it with --tenant for local development (always warns, see --tenant CLI override), or by confirming the token actually carries the configured claim:
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:
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, for one of two independent reasons:
The caller’s role doesn’t grant run_sql: true. No connection-level fix resolves this: the role itself has to change.
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.
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.