Skip to content

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.

database:
url: ${DATABASE_URL}
schemas: [public]
adapter:
type: api
uiHints:
path: ./ui-hints.yaml

Kozou looks for kozou.config.yaml in the current working directory by default. Point any command at a different path with --config:

Terminal window
kozou inspect --config ./config/kozou.config.yaml
kozou dev --config ./config/kozou.config.yaml

The PostgreSQL connection and the schemas Kozou introspects.

FieldTypeDefaultDescription
urlstring— (required)PostgreSQL connection string. Required, but if it is omitted from the file the loader fills it from the DATABASE_URL environment variable.
schemasstring 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.

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.

FieldTypeDefaultDescription
typeapi | postgrestapiThe backend the Admin UI runs against: api (the in-house @kozou/api, the default) or postgrest (an external PostgREST, opt-out).
urlstringhttp://postgrest:3000Base URL of the PostgREST endpoint. Only consumed when type: postgrest; ignored by the in-house api backend, which serves REST in-process.
adapter:
type: api

Since 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.

FieldTypeDefaultDescription
rpc.allowDefinerstring[][]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.allowPublicExecutestring[][]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.search

Exposure 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.

FieldTypeDefaultDescription
jwt.secretstringHS256 shared secret. Provide exactly one of secret, publicKey, or jwksUri.
jwt.publicKeystringRS256 public key (PEM).
jwt.jwksUristringA provider’s remote JWKS endpoint (Auth0 / Clerk / Supabase); keys are selected by kid, cached, and refreshed on rotation.
jwt.algorithmsstring arrayAllowed signing algorithms (HS256 / RS256).
jwt.issuerstringExpected iss claim, when set.
jwt.audiencestring or arrayExpected aud claim, when set.
roleClaimstringroleThe claim naming the PostgreSQL role to assume.
allowedRolesstring arrayOnly these roles may be assumed; a role outside the list gets 403.
defaultRolestringRole assumed when the token omits roleClaim.
anonRolestringRole for requests with no token. Unset → no-token requests get 401.
claimsGucstringrequest.jwt.claimsThe runtime setting the verified claims are published under.
ui.rolestringRole the bundled Admin UI runs as (HS256: the CLI mints its token claiming this role).
ui.tokenstringA 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 instead

A 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.

Where to find the UI hints file that refines the emitted Admin UI.

FieldTypeDefaultDescription
pathstring or nullnullPath to a UI hints YAML file. When null, Kozou relies on COMMENT-derived hints only.
uiHints:
path: ./ui-hints.yaml

UI 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.

Bind hosts and ports for the Admin UI and the MCP server that kozou dev starts.

FieldTypeDefaultDescription
ui.portinteger3333Port for the Admin UI.
ui.hoststring0.0.0.0Bind host for the Admin UI.
mcp.http.portinteger3334Port for the MCP HTTP server.
mcp.http.hoststring0.0.0.0Bind host for the MCP HTTP server.
mcp.stdiobooleanfalseWhether the MCP server uses the stdio transport.
mcp.execution.enabledbooleanfalseOpt in to the MCP call execution tool. Default off = describe-only.
mcp.execution.rolestringDatabase role every call runs as (via SET LOCAL ROLE). Required when enabled. Use a dedicated least-privilege role.
mcp.execution.claimsobjectFixed claims published for row-level security (under request.jwt.claims).
mcp.execution.allowstring[]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.

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).

FieldTypeDefaultDescription
respectPrivilegesbooleanfalseEvaluate a role’s effective table/column GRANTs during introspection and reflect them in the surfaces.
rolestringThe 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: analyst

Each surface treats the result in the way that fits it:

  • Admin UI hides what the role cannot reach — a table it cannot SELECT drops from the nav, a column it cannot INSERT is read-only on create, one it cannot UPDATE is read-only on edit, and Delete is hidden where the role lacks it.
  • MCP describe_table / describe_view and kozou docs annotate instead of hiding — every relation stays and is labelled with the role’s effective SELECT / INSERT / UPDATE / DELETE (plus per-column insertable / updatable on tables), and kozou docs grows 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.

FieldTypeDefaultDescription
ttlMsinteger60000Schema Context cache time-to-live, in milliseconds.
cache:
ttlMs: 60000

The 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.

The loader expands placeholders in string values against the process environment at load time, before validation. Two forms are recognized:

FormBehavior
${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.

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:

Terminal window
DATABASE_URL=postgres://kozou:kozou@localhost:5432/kozou \
kozou inspect --format yaml

The kozou CLI consumes DATABASE_URL, not KOZOU_DATABASE_URL. Reference ${DATABASE_URL} (or any variable you choose) explicitly from database.url if you prefer to be explicit.

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.

kozou.config.yaml
#
# 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: 60000

Applied 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.

  • kozou dev — the command that reads server, adapter, auth, and cache to run the Admin UI and MCP server.
  • UI hints — the file uiHints.path points at.
  • The @kozou/api REST layer — the in-house REST backend (the default) and the auth config it reads.