App Ingredients: Build the Boundary Before the Features
One small, production-shaped KDCube app — surface, contracts, identity, state, tests, release. Twelve stamped operations, from app card to shipped.
One small, production-shaped KDCube app with a useful surface, explicit provider and consumer contracts, non-blocking execution, correct identity and state boundaries, focused tests, and a releasable package.
You do not need to start with chat, an agent, a UI, MCP, storage, and jobs at once. Start with the smallest capability the product owns. Add another ingredient only when the same app should own its behavior, authority, state, and release lifecycle.
Current code and descriptors still say bundle in names
such as bundle_id, bundles.yaml, and
@bundle_entrypoint. In this recipe, app =
bundle: one deployable KDCube runtime unit.
- A running KDCube deployment (local is fine)
- One capability the product truly owns
- The app descriptor:
bundles.yaml+ its templates - The reference doc, open in a tab (linked below)
The ingredient map
APP BOUNDARY | +-- provides API · MCP · widget · main view · agent · cron +-- consumes event lane · jobs · Data Bus · tools · MCP · named services +-- executes async in shared proc · explicit fence only when needed +-- identifies actor · storage user · platform authority · economics subject +-- stores props/secrets · Redis · Postgres · artifacts · shared files +-- communicates conversation events · Data Bus · comm · recording · telemetry +-- governs roles/grants · delegation · firewall · economics `-- maintains interface · config · docs · journal · tests · release notes
The detailed reference behind this recipe is What I Should Know Before Writing a KDCube App.
OP 10OF 120 Write the app card
Before code, write two lists.
THIS APP PROVIDES one authenticated status API one main agent called my-app.main THIS APP CONSUMES web_search and web_fetch the mem named-service namespace (read/search only) THIS APP OWNS deployment config: feature.enabled, request_timeout_s no app database tables yet no public callback yet
This prevents a common design failure: implementing a capability without declaring who owns it or who may consume it.
Provider-side examples are @api, @mcp,
@ui_widget, @ui_main, one reactive agent door, and
@cron. Consumer-side examples are
@on_reactive_event, @on_job, Data Bus handlers,
tools, MCP servers, and named-service namespaces.
An agent crosses both directions. The app provides the agent to callers;
that agent consumes its allowed capabilities. Several agents share one
reactive door and are dispatched by stable agent_id.
OP 20OF 120 Create a maintainable package
Keep the root small. Put implementation in named modules.
my-app@1-0/ entrypoint.py thin composition root README.md app purpose, surfaces, verification AGENTS.md implementation map and invariants release.yaml release metadata agents/ agent construction, prompts services/ domain logic surfaces/ API/MCP/UI adapters when useful events/ event normalization and dispatch tools/ app-local tools ui/ widgets/main view skills/ app-local skills config/ bundles.template.yaml bundles.secrets.template.yaml interface/ README.md my-app.openapi.yaml when the app has HTTP docs/ storage/README.md journal/ tests/
Only create folders this app needs. entrypoint.py declares
and composes; it does not become the whole application.
decorators == interface declaration == descriptor keys and gates == README surface inventory == focused tests == journal entry == release note when behavior changes
Keep this invariant in every functional change: decorators, interface declaration, descriptor keys, README inventory, tests, journal, and release notes agree.
The journal is the quick onboarding history for the next developer or coding agent. Record architecture, interface, state, and behavior decisions, not every mechanical edit.
OP 30OF 120 Make the first surface async
This is a complete app surface:
from typing import Any from kdcube_ai_app.apps.chat.sdk.solutions.chatbot.entrypoint import BaseEntrypoint from kdcube_ai_app.infra.plugin.bundle_loader import api, bundle_entrypoint, bundle_id BUNDLE_ID = "my-app@1-0" @bundle_entrypoint(name="my-app", version="1.0.0", priority=10) @bundle_id(id=BUNDLE_ID) class MyApp(BaseEntrypoint): @api( method="GET", alias="status", route="operations", user_types=("registered",), ) async def status(self, **_: Any) -> dict[str, Any]: return {{"ok": True, "app": BUNDLE_ID}}
Every platform callback and every I/O call behind it is async. KDCube
apps run inside the shared concurrent proc event loop. A synchronous HTTP,
filesystem, database, Redis, subprocess, sleep, or lock operation blocks
unrelated users and apps even when it is hidden inside
async def.
Use async clients. For a bounded dependency with no async API, use
await asyncio.to_thread(...). Put long CPU work, generated
code, or operational work behind an explicit job, venv, or fenced execution
boundary.
OP 40OF 120 Declare policy and consumer inventory
Decorators declare that a surface exists. Descriptor policy controls exposure, and consumer configuration limits what the app or agent may call.
items: - id: my-app@1-0 config: feature: enabled: true surfaces: as_provider: bundle: visibility: allowed_roles: - kdcube:role:registered as_consumer: default_agent: main agents: main: tools: - id: web kind: python module: kdcube_ai_app.apps.chat.sdk.tools.web_tools alias: web_tools allowed: [web_search, web_fetch]
Use user_types for ordered platform levels. Use raw/custom
roles such as kdcube:role:finance-team for role membership. Use
authority and grant checks for delegated or managed boundaries. These are
separate dimensions; do not turn an authority grant into a guessed role.
OP 50OF 120 Bind identity; never accept it from the model
The runtime binds tenant, project, actor, user, session, app, conversation, turn, and authority before app code runs. Request/model fields may locate an object; they cannot choose the effective identity.
For external channels or automation, verify the incoming actor and resolve an explicit delegation edge when the protected boundary requires another authority. Keep these subjects distinct when needed:
actor who called storage subject whose user-scoped data is addressed platform subject whose roles/grants are checked economics subject whose quota/funding pays
Cross-runtime context preserves provenance. It does not grant every downstream operation. The trusted tool, MCP, named-service, or provider boundary checks the authority and grants it requires again.
OP 60OF 120 Put state in its real home
| State | Correct home |
|---|---|
| App non-secret config | bundles.yaml; read with self.bundle_prop(...) |
| App secret | bundles.secrets.yaml / secret provider; await get_secret("b:...") |
| User non-secret app state | await get_user_prop(...), await get_user_props(), await set_user_prop(...), await delete_user_prop(...) |
| User secret/connected token | user-secret and connections helpers |
| Small distributed cache/dedupe/lease | runtime Redis/KV, correctly namespaced |
| Relational domain state | app-prefixed tables in the shared tenant/project Postgres schema |
| Durable backend-neutral artifacts | BundleArtifactStorage |
| Shared filesystem tree/index/checkout | below self.bundle_storage_root() |
| Conversation transcript/files | KDCube conversation/file contracts |
Do not use environment variables or module globals as app configuration
or state. Use bundle_call_context only for small JSON-safe
invocation metadata, not secrets, clients, files, or durable data.
Treat singleton: true as worker-local reuse, not persistence
or serialization. Singleton invocations may overlap and another worker can
serve the next request.
Provision Postgres tables idempotently under a
tenant/project/app-scoped advisory critical section. KDCube does not yet
provide a complete app-delete deprovision hook, so document table ownership,
retention, and operator cleanup in docs/storage/README.md.
OP 70OF 120 Choose one delivery contract
| Need | Contract |
|---|---|
| Direct request/response | API/operation |
| Ordered context for an agent | conversation event bus |
| Durable app-owned mutation/message | Data Bus |
| Ready work after durable state exists | background job stream |
| Peer/session/project progress | communicator event |
| Observation after work happened | recording/telemetry |
The conversation queue carries a wake; accepted events live in the ordered conversation/agent lane. ReAct can fold eligible events into a live turn. A run-to-completion agent receives a fixed start batch and later events become a later turn.
Data Bus writes are retryable and require idempotency/revision handling. Background jobs are also retryable; the durable domain record is the source of truth, not the queued envelope.
For a webhook that starts a conversation, verify proof, submit through
ChatIngressSubmitter, and reply early. Do not hold the webhook
open until the agent completes.
OP 80OF 120 Reuse the runtime before adding code
- Use built-in web, exec, rendering, file-hosting, and context tools directly. Their native paths already carry isolation, accounting, artifacts, citations, and provenance.
- Connect MCP once and allow only selected tools per agent. The trusted runtime resolves credentials; the restricted executor does not receive them.
- Expose ordinary MCP without creating a named service. Add a named service only when the domain also needs provider-owned refs, schemas, search, actions, materialization, and generic UI/agent behavior.
- Use the namespace provider's contract instead of teaching chat, canvas, or a scene to guess foreign ref semantics.
- Use
@venvonly for app-specific dependencies absent from proc requirements.
OP 90OF 120 Wrap agents in the KDCube conversation
Keep two stores correct:
agent working memory/checkpointer what the framework restores for the next model turn KDCube conversation record ordered turns · chat list/reload/search · files/artifacts · cost/time
Key the agent thread by KDCube conversation_id, rebuild
per-turn graphs from durable state, map progress to the communicator, and
use the framework-neutral conversation recorder. Do not keep a mutable graph
as singleton state.
For Claude Code, use the SDK integration instead of local subprocess glue. It binds user/conversation/agent continuity, streams through the communicator, accounts usage, and can preserve the native session/transcript in Git across workers and restarts.
OP 100OF 120 Guard spend and outbound data
For every paid API, tool, MCP, search, agent, cron, or job surface:
- resolve the economics subject from the bound authority context;
- choose a stable accountable
scope_id; - enter the economics guard with an estimate;
- run tracked services inside it;
- settle actual usage and handle denial/degradation.
A paid call is not accountable merely because it runs in KDCube. It must emit tracked usage under the right scope.
Use the outbound event firewall to decide which communicator events may leave the app. Recording captures approved post-firewall events; keep it bounded and use an async sink. Telemetry records facts that happened. It does not replace accounting, Data Bus, jobs, or the conversation event lane.
OP 110OF 120 Verify the app as deployed
Test more than the Python method:
- interface discovery and descriptor aliases match;
- API/widget role, user-type, authority, and grant denial paths work;
- no sync I/O runs in platform callbacks;
- duplicate Data Bus/job delivery is harmless;
- shared initialization survives two concurrent workers;
- config reload changes effective props without restart-only state;
- restart/scale-out preserves durable app and agent state;
- webhook returns early and the submitted turn is processed;
- fenced execution cannot access undeclared credentials or storage;
- paid work is admitted, attributed, recorded, and settled;
- outbound events are filtered and recorded as intended.
OP 120OF 120 Maintain and release the whole contract
Use the canonical sequence:
- Assemble With SDK Building Blocks
- Write a KDCube App
- Configure and Run the App
- Avoid Common Integration Failures
- Release App Content
Before release, update code, interface declarations, descriptor templates, README, storage docs, journal, tests, and release notes together. A version tag on drifting contracts is not a release.
- The app provides one useful, declared surface.
- Every runtime path is async and non-blocking.
- Provider policy and consumer inventory are explicit.
- Identity and delegation are host-bound and fail closed.
- Configuration, secrets, invocation context, and durable state use their correct homes.
- Singleton assumptions and shared mutations are concurrency-safe.
- The chosen bus/job/event contract matches the work semantics.
- Paid work and outbound data are governed.
- Conversation and framework memory survive restart when the app has an agent.
- Interfaces, config, docs, journal, tests, and release notes agree.
Build one boundary cleanly. Then add the next ingredient.