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 | 0.0.0.0 | Bind host for the Admin UI. |
mcp.http.port | integer | 3334 | Port for the MCP HTTP server. |
mcp.http.host | string | 0.0.0.0 | Bind host for the MCP HTTP server. |
mcp.stdio | boolean | false | Whether the MCP server uses the stdio transport. |
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: 0.0.0.0 mcp: http: port: 3334 host: 0.0.0.0 stdio: false # 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. The defaults bind 0.0.0.0 so the ports are reachable from outside a container (for example, through the Docker Compose port mapping). On a machine reachable from the network, remember that the in-house @kozou/api backend runs unauthenticated unless you configure auth — enable JWT + RLS, bind to a loopback host, or place it behind your own gateway when that matters.
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.
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.
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) host: 0.0.0.0 # bind host; 0.0.0.0 is container-reachable mcp: http: port: 3334 # MCP HTTP server port (kozou dev) host: 0.0.0.0 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.