canonic.yaml is validated against CanonicConfig at load time. Unknown top-level fields are ignored rather than rejected. Only version and project are required: everything else defaults.
Top-level fields
version and project are the only fields canonic actually needs to boot. Everything else configures a specific subsystem and can be left out entirely for a local, single-user setup: connections is empty until you add a data source, llm is null until you configure a provider, and the rest fall back to safe local defaults. embeddings and telemetry rarely need touching, they exist mostly for opt-in tuning and privacy control rather than day-to-day project setup.
connections[]
A connection is how canonic reaches an actual data source.
id is the name every other file uses to point back at it, type picks which connector implementation handles it, and params carries whatever that connector needs to connect, host and port for a warehouse, a manifest path for dbt. credentials_ref keeps the secret itself out of canonic.yaml entirely, instead of a password you write a pointer to where the password actually lives.
credentials_refmust be a reference, one ofenv:,keyring:,file:, orprovider:, never a literal secret. Config validation rejects a literal outright.llm.api_key_refand the MCP auth refs accept the same schemes minusprovider:, see Dynamic credentials below.- Postgres/Redshift connections additionally recognize
schema/schemas(search path) andtables(glob patterns narrowing introspection), plusfetch_column_stats: trueto merge zero-scan cardinality/null-ratio stats into the returned schema (a no-op on SQLite/DuckDB). dbtconnections recognizemanifest_path(path to the compiledmanifest.json, defaultmanifest.json) andtarget_connection(the id of the physical, queryable connection whose tables this manifest describes, see Connectors).target_connectionmust name a connection actually declared inconnections[]. An unknown id fails config validation at load time. Omitting it falls back to the dbt connection’s own id.
Dynamic credentials
env:, keyring: and file: all resolve once, to a fixed string that stays valid until someone rotates it by hand. That does not fit a credential the cloud provider issues with a built-in expiry. A Redshift IAM credential lives roughly 15 to 60 minutes, so a connection string built when the daemon starts is dead well before the daemon is.
provider:<name> covers that case. The name selects a registered credential provider, which fetches a fresh credential from the issuer and reports when it expires. Canonic caches it and refetches shortly before it runs out, so a long-lived daemon keeps working without an operator touching anything.
canonic.yaml. cluster_id, db_user and region are not secrets, they tell the provider how to fetch a credential, the same way a file: path is not a secret.
Things worth knowing before you reach for it:
- Redshift only, today.
provider:refs are resolved on every connect, and Redshift is the only connector that does that so far. Pointing another connector at one fails with a clear error rather than silently freezing the first credential it ever saw. - Not for inbound auth.
llm.api_key_ref,mcp.auth.tokens[].token_refandmcp.auth.oauth.client_secret_refare resolved once at startup and held. A provider credential there would go stale rather than refresh, so config validation rejects it. aws-iam-redshiftneeds boto3, which canonic does not install.pip install boto3. Credentials for the AWS call itself come from the standard boto3 chain (instance role,AWS_PROFILE, environment).- Rejected under
air_gapped: true. Fetching from AWS is network egress, which is what that mode exists to prevent. - Nothing is written to disk. A fetched credential lives in memory for the life of the process that fetched it. A CLI invocation and a running daemon each fetch their own.
llm
Each provider has a fixed credential requirement, enforced at load.
openai_compatible requires base_url, openai and anthropic require api_key_ref, and github_copilot forbids one entirely since it authenticates through a device-code flow that never touches canonic.yaml. Getting this wrong fails config validation immediately rather than surfacing as a runtime error the first time canonic tries to call the model.
reconcile
reconcile only governs how eagerly canonic ingest is allowed to write changes back to semantics/. It has no effect on what evidence gets collected, only on whether a resulting proposal can be applied without a human looking at it first. See Ingestion & reconciliation for how proposals are generated in the first place.
feedback
See Feedback loop (E11) for how these thresholds are used.
telemetry
canonic audit --telemetry-send only sends when all of enabled, endpoint, and transport_acknowledged are set, and runtime.air_gapped is false. Missing any one of them fails closed with a structured error (telemetry_not_configured). Nothing is ever sent implicitly. See Instrumentation & evaluation for the full payload shape and content-safety guarantees.
runtime
When
air_gapped: true, load-time validation additionally rejects a public llm.base_url, telemetry.enabled: true, and any credentials_ref/api_key_ref using a non-local scheme. The check runs once, at config load, so there’s no window where a project starts up in a state that could leak context off the machine. Telemetry has no allowlist path the way llm.base_url does via allow_cidrs. Under air_gapped, it is hard off regardless of telemetry.endpoint/transport_acknowledged.
mcp
This block only matters for
canonic mcp start --transport http. The default stdio transport is a local subprocess and needs no auth, process-level trust is enough. http transport is network-reachable, though, so it refuses to start unless at least one mechanism resolves: a token here (or via the --token-ref CLI override) and/or auth.oauth. When both are configured, a request is checked against the static token map first (no network call), falling through to OAuth verification if no static token matches: the two stay independently revocable, a token entry by editing canonic.yaml, an OAuth-issued token at the IdP. canonic mcp status reports which mechanism(s) are active on a running daemon. See Connecting your agent for the full remote-deployment walkthrough.
auth.oauth.verify_id_token matters more than its default suggests. In proxy mode, OIDCProxy verifies the upstream IdP’s access token by default, but many IdPs (Google, GitHub, some Okta setups) issue that as an opaque, non-JWT string, which fails verification outright rather than just producing a poor client identity. Setting verify_id_token: true verifies the id_token instead, which the OIDC spec guarantees is always a standard JWT carrying sub/email claims. This also controls what ends up as client_id in .canonic/events.jsonl: with it off, client_id comes from the access token’s client_id/azp/sub claim (often an opaque subject id). With it on, from the id_token’s, which is far more likely to be a meaningful identity like an email address.Reserved directories
Independent ofcanonic.yaml fields, every project scaffolds four committed context directories plus a git-ignored .canonic/ local-state directory (created with 0o700 permissions):
contracts/policies/ (tenancy.yaml / roles.yaml) is optional. Its presence is the feature switch for tenancy & access control, and a project without it behaves exactly as it did before that layer existed.
raw-sources/<connection-id>/evidence.jsonl
Not a hand-authored config file: it’s a deterministically-sorted, one-JSON-object-per-line snapshot of the evidence ingestion collected for a connection, written by DiskSnapshotStore so re-running ingestion against the same source yields a byte-identical file. Each line validates against EvidenceItem:
This is what
canonic ingest reads to draft proposals against semantics/. You’d only ever read it, not write it.