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.
The config.json pipeline
Section titled “The config.json pipeline”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_connectormust reference a connector declared inrequired_connectors/optional_connectors. - Rule F —
creator_valueandfed_by_connectorare 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, neithercreator_valuenordefault. An actor that ships a secret value fails. - Rule H —
ordermust 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.
The annotations, end to end
Section titled “The annotations, end to end”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_bydecides who fills the field: the buyer (user) or the platform (platform). Platform-managed fields are normally hidden.creator_valueis you, the kreator, prefilling a value — a default timeout, a template ID. Paired withmanaged_by: "platform"+ visible per Rule B.fed_by_connectordeclares 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 acreator_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.
The verification loop
Section titled “The verification loop”After any config or code change, run the loop in this order:
# 1. Lint — zero tolerance, the fleet lints cleanruff check src/
# 2. Emit + freshness check in one targetmake 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 actorpytestAlso 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— yourPINNED_PHRASESstill appear in the prompt.
Finally, confirm what the platform will actually see:
curl http://localhost:8080/api/actor/kaoohi-config | python -m json.toolThat 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.
Updating your actor
Section titled “Updating your actor”When kaboum ships fixes, pull them in:
cd /path/to/my-actorcopier update --trustCopier 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.
.rej conflicts
Section titled “.rej conflicts”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:
- Open the
.rejfile to read the template’s change. - Apply it by hand to the main file, keeping your domain logic.
- Delete the
.rejfile. - 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 Fieldfrom 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.