Skip to content

Track A — Config and verify

Every actor ships a single canonical artifact for the marketplace: src/kaoohi/config.json. The platform reads it out of your container image at registration, renders the kompanion deployment form from it, and bases its safety review on it. This page explains where it comes from, why you never edit it by hand, and the loop that keeps it honest.

config.json is derived. Two inputs, one emitter:

src/config.py Settings fields ─┐
(x-kaoohi-* annotations)├─→ src/kaoohi/config_emitter.py ─→ src/kaoohi/config.json
[tool.kaoohi] in pyproject.toml ─┘ (emit + validate) (checked in, derived)

The emitter walks every Settings field via Pydantic model_fields (including mixin-inherited ones), maps types, reads the x-kaoohi-* annotations, and merges the result with the [tool.kaoohi] marketplace block. The output is validated against the vendored src/kaoohi/schema.json plus cross-field tuning rules A–H — the ones you will actually hit:

  • Rule A — a required, user-managed item cannot be hidden: the user would have no way to supply it.
  • Rule B — a platform-managed item that is visible must carry a creator_value (your value, shown as a prefilled default).
  • Rule C — fed_by_connector must reference a connector declared in required_connectors / optional_connectors.
  • Rule F — creator_value and fed_by_connector are mutually exclusive: the deploying account’s credential and the actor author’s value cannot both claim a field.
  • Rule G — a secret-typed item must never carry a value, neither creator_value nor default. An actor that ships a secret value fails.
  • Rule H — order must be unique within a section; the platform sorts the form on it.

A violation is not a warning: the Dockerfile has a dedicated kaoohi-config stage that re-runs the emitter with --check during the image build, so a malformed config fails the build, before anything ships. The same file is baked into the image and served at GET /api/actor/kaoohi-config.

Each Settings field’s json_schema_extra speaks the x-kaoohi-* namespace:

Annotation Purpose
x-kaoohi-section Form section id (must exist in KAOOHI_SECTIONS)
x-kaoohi-managed-by "user" (buyer fills it) or "platform" (orchestrator injects it)
x-kaoohi-ui Widget (text, textarea, password, select, radio, slider, switch, number), order, options, step, placeholder, show_if, display
x-kaoohi-fed-by-connector Connector slug whose credential supplies this field at deploy time
x-kaoohi-creator-value The actor author’s prefilled value for a visible platform-managed field
x-kaoohi-required Override Pydantic’s requiredness in the emitted item
x-kaoohi-validation min/max/enum/pattern rules passed through to the form
x-kaoohi-env-var Override the env var name (default: field name uppercased)

Three concepts do the real work:

  • managed_by decides who fills the field: the buyer (user) or the platform (platform). Platform-managed fields are normally hidden.
  • creator_value is you, the kreator, prefilling a value — a default timeout, a template ID. Paired with managed_by: "platform" + visible per Rule B.
  • fed_by_connector declares that the deploying account’s connected credential supplies the field — an n8n URL, a mailbox token. The connector must be declared (Rule C) and cannot compete with a creator_value (Rule F).

A password widget maps to type secret, which triggers Rule G — so secret fields are SecretStr | None = Field(default=None, ...) with no value shipped, ever.

After any config or code change, run the loop in this order:

Terminal window
# 1. Lint — zero tolerance, the fleet lints clean
ruff check src/
# 2. Emit + freshness check in one target
make kaoohi-config
# = python -m src.kaoohi.config_emitter
# + python -m src.kaoohi.config_emitter --check (fails if stale)
# 3. Tests — unit tests plus the contract tests vendored in every actor
pytest

Also useful: make kaoohi-config-check (just the freshness gate) and make kaoohi-config-stdout (print the config without writing). The vendored suite includes the tests that protect the contract:

  • test_contract.py — the actor’s OpenAPI must satisfy the required cross-actor contract (models and health endpoints included).
  • test_kaoohi_config_emitter.py — emitter behavior, including rule enforcement and [tool.kaoohi] validation.
  • test_system_prompt.py — your PINNED_PHRASES still appear in the prompt.

Finally, confirm what the platform will actually see:

Terminal window
curl http://localhost:8080/api/actor/kaoohi-config | python -m json.tool

That serves the baked package resource — the same bytes the marketplace extracts from your image. After emitting, commit the derived src/kaoohi/config.json alongside the source change that produced it; a stale file fails the image build later, which is the right failure mode.

When kaboum ships fixes, pull them in:

Terminal window
cd /path/to/my-actor
copier update --trust

Copier replays your recorded answers from .copier-answers.yml — framework, integration, and marketplace choices are kept, not reset to defaults — and applies a 3-way merge: the version you generated from, your modifications, and the new template. Files you never touched update cleanly.

Where the template changed a file you also changed, Copier writes a .rej file (a rejected patch) next to the source and leaves your version in place. Resolve each one deliberately:

  1. Open the .rej file to read the template’s change.
  2. Apply it by hand to the main file, keeping your domain logic.
  3. Delete the .rej file.
  4. Run the verification loop above before moving on.

The conflicts you will see most are exactly the ones the extension points were designed to avoid: edits to template-owned prompts instead of ROLE_PLAYBOOK, or dozens of domain fields in src/config.py section 4.

The escape hatch: include_actor_config_mixin

Section titled “The escape hatch: include_actor_config_mixin”

Heavily-customized actors accumulate dozens of Settings fields — and every copier update re-renders src/config.py, so those fields conflict every time. The mixin flag moves them into an actor-owned file the template scaffolds once and never rewrites:

# src/config_actor.py (actor-owned — copier update never conflicts here)
from pydantic import Field
from pydantic_settings import BaseSettings
ACTOR_KAOOHI_SECTIONS = [
{"id": "routing", "name": "Routing", "description": "Call routing rules"},
]
class ActorSettingsMixin(BaseSettings):
routing_timeout: int = Field(
default=30,
description="Seconds before an unanswered call is escalated",
json_schema_extra={
"x-kaoohi-section": "routing",
"x-kaoohi-managed-by": "user",
# 200+ range: template orders stop below 100, so you never collide
"x-kaoohi-ui": {"widget": "slider", "order": 200},
},
)

src/config.py imports ActorSettingsMixin and the config class inherits it; mixin fields load from env and flow into the emitted config.json exactly like template fields. Your sections append after the template’s, deduped by id — you can add sections but never redefine a template one.

Enable it at generation (include_actor_config_mixin=true) or when a config conflict has already cost you an afternoon. Most actors never need it: the seven extension points plus ROLE_PLAYBOOK exist precisely so the common path stays conflict-free.

With config verified and updates tamed, the remaining loop is build and publish — covered in Track A’s deployment pages and kaboum’s configuration reference for the full parameter catalog.