kozou.config.yaml
kozou.config.yaml is the one file every Kozou command reads. kozou inspect, kozou mcp, and kozou dev all load it to find your database, decide how records are read and written, locate your UI hints, and pick the ports the Admin UI and MCP server listen on. create-kozou writes a fully-commented copy into the project it scaffolds.
Every field has a default. Kozou is designed to start with nothing but a DATABASE_URL environment variable set — if no config file is present, the loader falls back to that variable for the database connection and fills in every other field from its built-in defaults.
For the commands that consume it, see kozou dev. For the UI hints file it points at, see UI hints.
Synopsis
Section titled “Synopsis”database: url: ${DATABASE_URL} schemas: [public]
adapter: type: api
uiHints: path: ./ui-hints.yamlKozou looks for kozou.config.yaml in the current working directory by default. Point any command at a different path with --config:
kozou inspect --config ./config/kozou.config.yamlkozou dev --config ./config/kozou.config.yamlSections
Section titled “Sections”database
Section titled “database”The PostgreSQL connection and the schemas Kozou introspects.
| Field | Type | Default | Description |
|---|---|---|---|
url | string | — (required) | PostgreSQL connection string. Required, but if it is omitted from the file the loader fills it from the DATABASE_URL environment variable. |
schemas | string array | [public] | The schemas Kozou introspects to build its Schema Context. |
database: url: ${DATABASE_URL} schemas: [public]database.url is the only field with no usable default. If it is absent from both the file and the DATABASE_URL environment variable, the loader rejects the config with an error.
adapter
Section titled “adapter”How Kozou reads and writes record data. The Admin UI never talks to PostgreSQL directly — it goes through a data adapter, which keeps the read/write boundary swappable.
| Field | Type | Default | Description |
|---|---|---|---|
type | api | postgrest | api | The backend the Admin UI runs against: api (the in-house @kozou/api, the default) or postgrest (an external PostgREST, opt-out). |
url | string | http://postgrest:3000 | Base URL of the PostgREST endpoint. Only consumed when type: postgrest; ignored by the in-house api backend, which serves REST in-process. |
adapter: type: apiSince v1.0, Kozou reads and writes records through its in-house @kozou/api backend by default — it serves REST in-process under kozou dev, with no separate container.
To opt out and use an external PostgREST instead, set type: postgrest and point url at it (the scaffolded docker-compose.yml ships a PostgREST service commented out as an opt-out — uncomment it):
adapter: type: postgrest url: ${KOZOU_ADAPTER_URL:-http://postgrest:3000}You can also override the backend for a single run with --adapter postgrest (or --adapter api) on kozou dev. See The @kozou/api REST layer for the in-house backend’s endpoints, OpenAPI document, and security boundary.
Opt-in RPC actions (since v1.4) — Postgres functions tagged @expose: rpc in their COMMENT, served by the in-house @kozou/api backend as POST /rpc/<schema>.<fn>. The lists below are the extra deploy-time opt-in the riskier cases require; both hold schema-qualified function names and default to empty.
| Field | Type | Default | Description |
|---|---|---|---|
rpc.allowDefiner | string[] | [] | SECURITY DEFINER functions authorized for exposure. Such a function runs as its owner and can bypass RLS, so it needs this in addition to the @expose: rpc tag and an owner-safe SET search_path. |
rpc.allowPublicExecute | string[] | [] | Functions that intentionally keep EXECUTE granted to PUBLIC (anonymous-callable). Without this, a function still granting PUBLIC EXECUTE is hard-skipped rather than silently opened to everyone. @expose: rpc public is the per-function tag equivalent. |
api: rpc: allowDefiner: - public.approve_order allowPublicExecute: - public.searchExposure is opt-in and never silent: nothing is exposed without @expose: rpc, and a tagged function that fails a guardrail (residual PUBLIC EXECUTE, an unsafe definer search_path, an overloaded name, …) is loudly skipped, not quietly dropped. Whether a caller may actually run an action is enforced by the PostgreSQL EXECUTE privilege under the request’s role — exposure is not permission. The RPC wire shape is a stable contract as of Kozou v1.6. The opt-out PostgREST backend ignores this section (it uses PostgREST’s own /rpc/).
Optional JWT verification for the in-house @kozou/api backend. With no auth block, the backend runs unauthenticated on loopback. Add one to verify a signed JWT on each request: Kozou checks the token, then runs the request under SET LOCAL ROLE <role-from-claim> with the claims published to PostgreSQL, so your own row-level-security (RLS) policies decide what it reads and writes. A request carrying no token is rejected with 401 unless you set anonRole, in which case it runs under that anonymous role with empty claims. Kozou enforces; it does not issue identity — see Authentication and authorization.
| Field | Type | Default | Description |
|---|---|---|---|
jwt.secret | string | — | HS256 shared secret. Provide exactly one of secret, publicKey, or jwksUri. |
jwt.publicKey | string | — | RS256 public key (PEM). |
jwt.jwksUri | string | — | A provider’s remote JWKS endpoint (Auth0 / Clerk / Supabase); keys are selected by kid, cached, and refreshed on rotation. |
jwt.algorithms | string array | — | Allowed signing algorithms (HS256 / RS256). |
jwt.issuer | string | — | Expected iss claim, when set. |
jwt.audience | string or array | — | Expected aud claim, when set. |
roleClaim | string | role | The claim naming the PostgreSQL role to assume. |
allowedRoles | string array | — | Only these roles may be assumed; a role outside the list gets 403. |
defaultRole | string | — | Role assumed when the token omits roleClaim. |
anonRole | string | — | Role for requests with no token. Unset → no-token requests get 401. |
claimsGuc | string | request.jwt.claims | The runtime setting the verified claims are published under. |
ui.role | string | — | Role the bundled Admin UI runs as (HS256: the CLI mints its token claiming this role). |
ui.token | string | — | A ready-made token for the Admin UI (RS256 / external IdP, where the CLI cannot mint). |
auth: jwt: secret: ${KOZOU_JWT_SECRET} # HS256 — or publicKey (RS256), or jwksUri # publicKey: ${KOZOU_JWT_PUBLIC_KEY} # jwksUri: https://your-idp/.well-known/jwks.json # Auth0 / Clerk / Supabase algorithms: [HS256] issuer: my-issuer # optional audience: my-api # optional roleClaim: role # claim naming the DB role (default: role) allowedRoles: [app_reader, app_admin] # only these roles may be assumed defaultRole: app_reader # role when the token omits roleClaim anonRole: web_anon # role for requests with no token (else 401) ui: role: app_admin # role the bundled Admin UI runs as (HS256) # token: ${KOZOU_ADAPTER_TOKEN} # RS256 / external IdP: supply a token insteadA present-but-invalid token is always 401. The login role of database.url must be GRANTed membership in every allowed role (and in anonRole when set). Each field also has a KOZOU_JWT_* / KOZOU_UI_ROLE / KOZOU_ADAPTER_TOKEN environment-variable equivalent, used when the auth block is absent. The opt-out PostgREST backend is unaffected by this section.
uiHints
Section titled “uiHints”Where to find the UI hints file that refines the emitted Admin UI.
| Field | Type | Default | Description |
|---|---|---|---|
path | string or null | null | Path to a UI hints YAML file. When null, Kozou relies on COMMENT-derived hints only. |
uiHints: path: ./ui-hints.yamlUI hints layer on top of the conventions Kozou reads from your COMMENT tags (@ai, @widget, @policy, @example). See UI hints for the file format and how it composes with those tags.
server
Section titled “server”Bind hosts and ports for the Admin UI and the MCP server that kozou dev starts.
| Field | Type | Default | Description |
|---|---|---|---|
ui.port | integer | 3333 | Port for the Admin UI. |
ui.host | string | 127.0.0.1 | Bind host for the Admin UI. Loopback by default; a container sets 0.0.0.0 through KOZOU_UI_HOST. |
mcp.http.port | integer | 3334 | Port for the MCP HTTP server. |
mcp.http.host | string | 127.0.0.1 | Bind host for the MCP HTTP server. Loopback by default; a container sets 0.0.0.0 through KOZOU_MCP_HTTP_HOST. |
mcp.http.enabled | boolean | true | Whether the MCP HTTP endpoint is served at all. Set false to run the Admin UI and REST alone, with no MCP listener. Also settable as KOZOU_MCP_HTTP_ENABLED. |
mcp.http.advertisedUrl | string | — | The address MCP clients reach the endpoint at, when that is not what port says — a remapped published port, a tunnel, a devcontainer, WSL forwarding, a reverse proxy. Its path must be exactly /mcp. The Admin UI’s “Connect an AI agent” page hands it out as given instead of building a URL from your browser’s host. Also settable as KOZOU_MCP_HTTP_ADVERTISED_URL. |
mcp.stdio | boolean | false | Whether the MCP server uses the stdio transport. |
mcp.provenance | boolean | false | Opt in to add a provenance object (database version, Kozou version, schema build time) to read/describe tool results, so an answer can explain which definition produced it. Default off. |
mcp.execution.enabled | boolean | false | Opt in to the MCP call execution tool. Default off = describe-only. |
mcp.execution.role | string | — | Database role every call runs as (via SET LOCAL ROLE). Required when enabled. Use a dedicated least-privilege role. |
mcp.execution.claims | object | — | Fixed claims published for row-level security (under request.jwt.claims). |
mcp.execution.allow | string[] | — | Allowlist of schema-qualified function names the tool may run. Omit = every exposed function. |
server: ui: port: 3333 host: 127.0.0.1 mcp: http: port: 3334 host: 127.0.0.1 # Serve the MCP HTTP endpoint at all (default true). Set false to bring # up the Admin UI and REST with no MCP listener. enabled: true # Where clients reach the endpoint, when a port remap, tunnel or proxy # makes that different from the bind port above. Path must be /mcp. # advertisedUrl: http://localhost:4334/mcp stdio: false # Opt-in: add a provenance stamp (database version, Kozou version, build # time) to read/describe tool results (default off). # provenance: true # Opt-in: let the MCP `call` tool execute exposed RPC actions (default off). # execution: # enabled: true # role: kozou_mcp_agent # # claims: { tenant_id: acme } # # allow: [public.approve_order]kozou dev runs the Admin UI on server.ui and the MCP HTTP server on server.mcp.http. Both bind 127.0.0.1 by default, so neither port is reachable from another machine out of the box. The shipped Docker Compose stacks set KOZOU_UI_HOST / KOZOU_MCP_HTTP_HOST to 0.0.0.0 inside the container — a container’s loopback is its own, so a port mapping could not otherwise reach the listeners — and publish those ports on the host’s 127.0.0.1 only. Widening the bind host on a machine reachable from the network exposes surfaces that have no authentication of their own, and the in-house @kozou/api backend runs unauthenticated unless you configure auth — enable JWT + RLS, keep the loopback bind, or place it behind your own gateway when that matters.
server.mcp.http.enabled is on by default. Setting it to false opts out of the MCP HTTP endpoint entirely: kozou dev starts no MCP listener and builds no schema cache (that cache exists only to serve MCP there), kozou mcp --http refuses instead of contradicting the config, and the Admin UI drops its “Connect an AI agent” header link and dashboard card — a direct visit to /connect gets a 404 naming this key. The stdio transport is unaffected; server.mcp.http governs the HTTP endpoint only. An auth block under a disabled endpoint is rejected as dead configuration rather than one of the two being silently ignored. Adding authentication needs an authorization server, while declining to run an endpoint needs nothing — for a runtime that only ever serves the Admin UI, opting out is the cheaper posture.
server.mcp.http.advertisedUrl separates two facts that are the same only in the simplest deployment: the address the runtime binds (host and port) and the address clients reach. A remapped published port, a tunnel, a devcontainer, WSL forwarding or a reverse proxy makes them differ, and the Admin UI’s “Connect an AI agent” page cannot see the difference — left undeclared it builds the URL from your browser’s host plus the bind port, and hands out config for an address that is not the endpoint. Set this to the address clients should register and the page hands out that address, unchanged apart from surrounding whitespace.
It must be an absolute http/https URL with no query or fragment, and its path must be exactly /mcp — the transport matches that path and nothing else, so a bare host, a trailing slash (/mcp/) or a sub-path (/kozou/mcp) are all refused at startup. This is a real limit on the proxy case: Kozou can be advertised behind a reverse proxy that passes the endpoint through at the origin’s root, but not behind one that mounts it under a path prefix.
It is rejected alongside server.mcp.http.auth — that block already declares the endpoint’s address as auth.resource, and resource is the one clients obey because they discover it from the endpoint’s own metadata — and rejected under a disabled endpoint, for the same reason an auth block is: nothing is served, so there is no address to advertise.
server.mcp.execution is off by default (the MCP server is describe-only). Enabling it exposes a call tool that runs exposed functions. Without server.mcp.http.auth, calls run as a single fixed role — no per-caller identity, so that mode is unsuitable for multi-tenant per-user authorization, and since the transport is then unauthenticated, keep it on a loopback host. With the opt-in OAuth resource-server block (server.mcp.http.auth), the endpoint requires a verified bearer token from your own identity provider on every request and call runs as each token’s role, restricted to an explicit allowedRoles allowlist. The block applies to the HTTP transport only — a stdio run ignores it. See Remote MCP with OAuth for the block’s keys and deployment guidance. See Executing actions from an MCP agent for the full execution guidance, including the SECURITY DEFINER caveat.
server.mcp.provenance is off by default. When enabled, every read/describe tool result carries an additive provenance object — databaseVersion (the PostgreSQL server version, truncated to major.minor), kozouVersion (the @kozou/mcp build that compiled the surface), and builtAt (when the schema context was last built) — so an agent’s answer can explain which schema version and definition produced it. The call execution tool is not stamped. It is additive to the stable MCP result shape; leaving it off keeps existing tool output byte-for-byte unchanged.
introspection
Section titled “introspection”Opt-in privilege-aware introspection: make the generated surfaces reflect what a role may actually do, not just what the schema declares. Off by default (schema-faithful).
| Field | Type | Default | Description |
|---|---|---|---|
respectPrivileges | boolean | false | Evaluate a role’s effective table/column GRANTs during introspection and reflect them in the surfaces. |
role | string | — | The role to evaluate. Defaults to the Admin UI’s role (auth.ui.role, else auth.defaultRole); set this to override, and required when a ready-made auth.ui.token is supplied (its role can’t be inferred). |
introspection: respectPrivileges: true role: analystEach surface treats the result in the way that fits it:
- Admin UI hides what the role cannot reach — a table it cannot
SELECTdrops from the nav, a column it cannotINSERTis read-only on create, one it cannotUPDATEis read-only on edit, and Delete is hidden where the role lacks it. - MCP
describe_table/describe_viewandkozou docsannotate instead of hiding — every relation stays and is labelled with the role’s effectiveSELECT/INSERT/UPDATE/DELETE(plus per-columninsertable/updatableon tables), andkozou docsgrows a per-table Security section. So an AI agent is told what it may touch rather than having unreadable tables vanish. See Connect an AI agent.
It reuses the privileges Kozou already reads (no extra queries) and is advisory only — enforcement always stays in PostgreSQL (the role’s GRANTs and your RLS policies).
How long the introspected Schema Context is cached before Kozou rebuilds it.
| Field | Type | Default | Description |
|---|---|---|---|
ttlMs | integer | 60000 | Schema Context cache time-to-live, in milliseconds. |
cache: ttlMs: 60000The default 60-second TTL means a schema change (for example, after running a migration) can take up to a minute to appear. Lower ttlMs while iterating on the schema; raise it once it is stable.
Environment-variable expansion
Section titled “Environment-variable expansion”The loader expands placeholders in string values against the process environment at load time, before validation. Two forms are recognized:
| Form | Behavior |
|---|---|
${VAR} | Replaced with the value of VAR. If VAR is unset, it expands to an empty string. |
${VAR:-default} | Replaced with the value of VAR, or default if VAR is unset. |
adapter: url: ${KOZOU_ADAPTER_URL:-http://postgrest:3000}With that line, Kozou uses $KOZOU_ADAPTER_URL when it is set, and falls back to http://postgrest:3000 when it is not.
To write a literal $, double it: $$ expands to a single $. So $${VAR} produces the literal text ${VAR} rather than expanding it.
Expansion is single-level by design: a value substituted in from the environment is never re-scanned. A secret that legitimately contains ${...} — for example, a ${ sequence inside a DATABASE_URL password — is preserved verbatim instead of being mistaken for another placeholder.
DATABASE_URL fallback
Section titled “DATABASE_URL fallback”If database.url is missing or empty in the file, the loader fills it from the DATABASE_URL environment variable. This is what lets the bundled template ship url: ${DATABASE_URL} and lets Kozou run from DATABASE_URL alone with no config file at all:
DATABASE_URL=postgres://kozou:kozou@localhost:5432/kozou \ kozou inspect --format yamlThe
kozouCLI consumesDATABASE_URL, notKOZOU_DATABASE_URL. Reference${DATABASE_URL}(or any variable you choose) explicitly fromdatabase.urlif you prefer to be explicit.
Environment overrides
Section titled “Environment overrides”Four server keys can also be set straight from the environment. Unlike ${VAR} expansion — which only reaches placeholders you wrote into the YAML — these are honoured even when there is no config file at all. That is what the container path needs: the shipped Compose stacks mount no kozou.config.yaml, so the environment is the only route to these values.
| Variable | Overrides | Notes |
|---|---|---|
KOZOU_UI_HOST | server.ui.host | Both shipped Compose stacks set 0.0.0.0 so the loopback-published port mapping can reach the Admin UI inside the container. |
KOZOU_MCP_HTTP_HOST | server.mcp.http.host | The same, for the MCP HTTP server. |
KOZOU_MCP_HTTP_ENABLED | server.mcp.http.enabled | false brings up the Admin UI and REST with no MCP listener. Both shipped Compose stacks forward it. |
KOZOU_MCP_HTTP_ADVERTISED_URL | server.mcp.http.advertisedUrl | The address clients reach the MCP endpoint at, when a port remap, tunnel or proxy makes that different from the bind port. Both shipped Compose stacks forward it. |
KOZOU_MCP_HTTP_ENABLED accepts exactly true or false (case-insensitive, surrounding whitespace ignored). Unset or empty means “no override”, leaving the config value in place. Any other spelling — 0, off, no — is refused with an error rather than defaulted: a flag that silently read as “on” because it was spelled 0 would keep an unauthenticated listener running while you believed you had turned it off.
${VAR} expansion is not a substitute for this variable even inside a config file, because expansion yields the string "false", which the schema rightly refuses for a boolean field.
When a config is refused, the CLI reports the environment variables a value was actually taken from alongside the file path (loaded from: /srv/app/kozou.config.yaml, with values from KOZOU_MCP_HTTP_ENABLED), so an environment-caused fault is not attributed to a file that never contained it.
Complete example
Section titled “Complete example”A fully-commented config covering every section. Every field shown here matches its default, so you can delete any line you do not need to change.
## Every field has a sensible default; only set what you need to override.# All ${VAR} and ${VAR:-fallback} placeholders are expanded against the# process environment at load time. Use $$ for a literal `$`.
database: # Connection string. Falls back to the DATABASE_URL env var when omitted. url: ${DATABASE_URL} # Schemas to introspect when building the Schema Context. schemas: [public]
server: ui: port: 3333 # Admin UI port (kozou dev) # Bind host; loopback by default. A container sets KOZOU_UI_HOST=0.0.0.0 # instead, so its (loopback-published) port mapping can reach the listener. host: 127.0.0.1 mcp: http: port: 3334 # MCP HTTP server port (kozou dev) host: 127.0.0.1 # Serve the MCP HTTP endpoint at all. false = Admin UI + REST only, with # no MCP listener (also settable as KOZOU_MCP_HTTP_ENABLED). enabled: true stdio: false # MCP stdio transport
adapter: # Backend the Admin UI runs against. "api" (default) is the in-house # @kozou/api, served in-process by kozou dev — no extra container. type: api # To opt out to an external PostgREST instead, set type: postgrest and # point url at it (add a postgrest service to docker-compose.yml): # type: postgrest # url: ${KOZOU_ADAPTER_URL:-http://postgrest:3000}
# auth:# # Optional. Without this block @kozou/api runs unauthenticated on loopback.# # Add it to verify a signed JWT and run each request under SET LOCAL ROLE# # <role-from-claim> so your RLS policies decide access.# jwt:# secret: ${KOZOU_JWT_SECRET} # HS256 — or publicKey (RS256), or jwksUri# roleClaim: role# allowedRoles: [app_reader, app_admin]# defaultRole: app_reader# ui:# role: app_admin
uiHints: # Path to the UI hints file, or null to rely on COMMENT tags only. path: ./ui-hints.yaml
cache: # Schema Context cache TTL in milliseconds. ttlMs: 60000Applied to a schema with products (a status column moving through draft / published / archived), orders, and an authors / books relation, this config introspects the public schema, serves the resulting Admin UI on http://localhost:3333, exposes MCP over HTTP on http://localhost:3334, and rebuilds its Schema Context at most once every 60 seconds.
Where to next
Section titled “Where to next”kozou dev— the command that readsserver,adapter,auth, andcacheto run the Admin UI and MCP server.- UI hints — the file
uiHints.pathpoints at. - The @kozou/api REST layer — the in-house REST backend (the default) and the
authconfig it reads.