KDCube is built around apps
Everything you ship on KDCube lives inside an app. An app is the versioned unit the runtime discovers, configures, and serves. It may host an agent, wrap an agent you already have, expose APIs or services, provide UI, run background work, or combine only the surfaces your product needs. The SDK includes the framework-neutral Agent Harness Runtime, the built-in ReAct agent, and reusable isolated execution, but none is required for an API-only, UI-only, or job-only app.
That single decision shapes most of the SDK's ergonomics:
- One synchronized application contract. Runtime decorators, interface declarations, configuration templates, storage ownership, tests, release metadata, and stable documentation describe the same app. UI, chat, MCP, named services, Data Bus handlers, jobs, and ReAct are conditional modules, not mandatory folders.
- Deployable and reloadable. Apps can come from Git or a mounted path and can be updated independently of the platform services that host them.
- Identity carried through the runtime. Admission resolves tenant, project, user, app, conversation, and turn context. Trusted app and tool code must use that carried context and the scoped SDK contracts; isolated generated code receives only its prepared workspace and supervisor bridge.
- Accountable execution. Model, search, tool, and other metered calls made through platform pathways produce spending events attributed to the user, app, conversation, turn, and agent.
- Framework-agnostic inside. An app can use the built-in descriptor-selected ReAct v2/v3 loop, LangGraph, CrewAI-authored flows, the separate ISO runtime for controlled execution, plain Python, or no LLM at all. The platform guarantees the surrounding runtime; what happens inside the app is your choice.
- Provider and consumer surfaces.
surfaces.as_providerdeclares what the app exposes.surfaces.as_consumerdeclares which tools, skills, MCP servers, and named-service namespaces its agents may call. An app can be a provider, a consumer, both, or neither.
Deployment-scope rule: one running deployment is bound to one effective tenant/project and may serve many users and operator-approved apps. Shared backing services can enforce logical namespaces, but stronger customer or lifecycle isolation requires separate deployments or dedicated infrastructure.
Terminology: KDCube now calls this unit an app. Internal identifiers and descriptors still use the older word bundle, for example @bundle_entrypoint, bundle_id, bundles.yaml, and /bundles/....
The rest of this page is a tour of how you actually build one: what an app contains, how it plugs into the platform, what the built-in primitives (storage, tools, app events, named services, hosted files, SDK integrations, widgets, APIs, @mcp, @cron, @on_job, @venv) look like, and how you deploy it.
What Is an App?
An app is your application package. It combines Python backend code with optional TypeScript UI widgets and views into a single deployable unit. The package becomes a KDCube app by subclassing the platform's base class (BaseEntrypoint or a derived variant) and registering with the current internal decorator @bundle_entrypoint.
An app is the end-to-end application unit inside one tenant/project environment. It can include backend execution logic, authenticated or public APIs, widget UI, main UI, scheduled jobs, background job handlers, named-service providers, deployment-scoped config, deployment-scoped secrets, and optional per-user state. One environment can host many apps at the same time.
App UI is a platform-served surface, not a special iframe concept. An app may expose widget UI with @ui_widget(...) and main UI with @ui_main / ui.main_view. KDCube builds and serves those assets through the integrations/static routes; a consuming frontend can render them directly or embed them, and the KDCube control plane often uses iframes for isolation. That iframe is a client display choice, not an app object.
App code can be delivered either from git or from a mounted local path. In deployed environments the registry usually points at a git repo, ref, and subdirectory. In local descriptor-driven development, use kdcube init --tenant T --project P --descriptors-location ... to stage a concrete runtime under the platform default base (~/.kdcube/kdcube-runtime/<tenant>__<project>/), then iterate with kdcube bundle reload <bundle_id> --tenant T --project P. Rebuild platform images later with kdcube refresh --tenant T --project P --build; add --path /path/to/kdcube-ai-app when refresh should copy a local platform checkout first, or add exactly one of --latest, --upstream, or --release <ref> when the existing runtime should move to another platform source while preserving descriptors. Reapply seed app descriptors with kdcube bundle config apply --descriptors-location ..., not with a full platform refresh.
The reference workspace app consumes the shared Agent Harness through the ReAct adapter. The deployment descriptor selects ai.react.react_agent_version; the checked-in reference assembly.yaml currently selects v2, while v3 is available when explicitly selected. ReAct provides tools, skills, continuous conversations, a source pool, ANNOUNCE, named-service tools, lifecycle events, followup, and steer. In safe_fanout mode, strategy traits govern whether ordered actions may coexist. Ordinary accepted actions preserve their order; a fully validated call with the supported neutral, detached execution profile may begin as soon as its streamed action closes and run in parallel with continued generation. Configure each agent under config.react.<agent_id>, with fallback to default_agent. Apps are not required to use ReAct: they can host LangGraph, another agent runtime, plain Python, or no LLM.
App Events, Policies, and Object Rehosting
App events are the authoring model for story-aware UI such as side chat, wizard, canvas, and snapshot flows. Tools are a special case of event sources: for a tool call, tool_id == event_source_id and tool_call_id == event_id. App UI can also submit authored external events, for example a saved wizard draft, uploaded evidence, canvas review request, or current snapshot notification.
| Origin | Event identity | Typical use |
|---|---|---|
| Tool call | event_source_id = tool_idevent_id = tool_call_id | Tool result becomes timeline blocks through event-source policies. |
| Widget or main UI event | payload.external_event.event_source_idpayload.target.agent_id | Wizard, canvas, and chat events land on the selected agent lane. |
| Externally tracked artifact ref | ext:... or another registered namespace | react.pull invokes the namespace rehoster and returns conversation-owned conv:fi: refs. |
Use explicit events for meaningful product transitions: saved draft, file attached, file deleted, assistance requested, canvas review requested, snapshot available, and chat message. Store fast-changing UI state through app APIs or app-owned storage, then send a compact event with a snapshot or artifact ref when the agent should see the state.
{
"payload": {
"target": {
"agent_id": "default.react.agent",
"story_kind": "case_wizard",
"story_id": "case:draft-123"
},
"external_event": {
"event_source_id": "case_workspace.wizard.assistance.requested",
"story_id": "case:draft-123",
"routing": {
"reactive": true,
"iteration_credit": 1
},
"data": {
"section_id": "observed_behavior",
"snapshot_ref": "ext:case-workspace/draft-123/snapshots/current.yaml"
}
}
}
}
Event-source policies are registered by phase. The first implemented production family is block_production, which converts a tool or external-event result into timeline blocks and artifact rows. The same source can bind later phases such as timeline_projection, announce_production, and compaction_projection so the app controls how its events appear in visible context, ANNOUNCE, and compaction.
Named-service providers extend this pattern from events to objects. A provider can expose search scopes, object schema, object actions, default open effects, and block.produce/block.render policies. react.pull materializes the object into the ReAct artifact space while preserving meta.object_ref, so react.read and timeline projection can select the namespace-owned policies.
Custom artifact namespaces are resolved by registered namespace rehosters. A rehoster materializes approved bytes into the current turn and returns a qualified conv:fi: ref. Physical destinations are role-based: editable project state under turn_<id>/git/projects/..., produced files under turn_<id>/files/..., workflow snapshots under turn_<id>/git/snapshots/..., uploads under attachments/..., and rehosted evidence under external/.... After react.pull(paths=["ext:..."]), agents continue with the returned logical ref or materialized path.
Example Apps
Start from the examples folder in sdk/examples/bundles/. The folder name still uses the internal term. The concrete reference app used on this page is workspace@2026-03-31-13-36 — a production-style sample that combines ReAct orchestration, app-local tools, widget decorators, REST operations, public endpoint examples, Telegram webhook and Mini App patterns, attachment handling, ReAct-stream-to-Telegram delivery, and custom UI. It is the current reference app for app authoring docs, but not the reference for @cron or @venv.
| App | Description |
|---|---|
workspace@2026-03-31-13-36 |
Reference app for this page. Demonstrates ReAct orchestration, app-local and MCP tools, decorators for widgets and APIs, public endpoint examples, Telegram hook and webapp patterns, attachment handling, economics-aware entrypoint patterns, a custom main view under ui/main/, widget UI under ui/widgets/, and integrations operations that a real UI can call. |
Platform Architecture for App Developers
See also: Platform Overview
KDCube is an AI application framework with an integrated, self-hosted runtime. One running deployment has one effective tenant/project scope and can serve many apps and concurrent users. Shared infrastructure may host several deployments, but each deployment keeps its own schemas, namespaces, prefixes, configuration, and runtime context.
An app is the deployable unit. It can host one or many agents, expose backend or browser surfaces, provide named services, consume other apps or MCP servers, run jobs, or simply wrap code you already operate. It does not have to be a chat app.
Big Picture: How It All Fits Together
Event Bus and Data Bus
These names describe different directions and must not be used as size classes. The Conversation Event Bus is ordered ingress into a conversation lane: follow-ups, steer, consent transitions, provider events, and other accepted inputs can wake or fold into an agent turn. The Data Bus carries app-owned domain commands to configured @data_bus_handler consumers. It is for application work and mutations, not a generic channel for “heavy data.”
Outbound live UI delivery uses ChatCommunicator, which emits post-firewall envelopes to SSE/Socket.IO clients and optional recorders. Files and provider objects use storage plus authorized refs; they do not become Data Bus messages merely because they contain bytes. Event declarations live in sdk/events, transport and wake lanes live in service communication, and runtime object resolution lives separately in the Agent Harness.
Classical Runtime and Agent Harness
sdk/runtime contains classical process/runtime facilities: tool execution, isolated and external execution, service clients, and process-local support. sdk/runtime/harness is the shared framework-neutral layer used by ReAct, hosted LangGraph and future adapters, conversation services, chat, canvas, and namespace integrations.
| Harness scope | Owns | Does not own |
|---|---|---|
harness/events | Authorized object/event-ref resolution and byte or action materialization | Event transport, scheduling, partitioning, or turn wake-up |
harness/workspace | Per-turn paths, conv:fi: refs, pull/materialization, change detection, and WorkspaceArtifact | Timeline ordering or model-facing ReAct tools |
harness/timeline | Event identity, conv.timeline.v1, TurnLog, turn views, and ownership-fenced provider projection | Communicator streams or Event Bus queues |
ReAct and hosted LangGraph are sibling harness consumers. Conversation persistence and search, including ContextRAGClient, belong to sdk/solutions/conversation. A ref is a locator, not a credential: tenant, project, user, authority, and grants come from trusted runtime context.
Two Services, One Platform
Ingress (chat-ingress)
Handles inbound chat traffic. Authenticates users, enforces rate limits, validates the target app, enqueues the task, and opens an SSE stream back to the client. Your app rarely needs to know about this — the platform wires it up automatically.
Processor (chat-proc)
Dequeues tasks, loads your app entrypoint, and calls execute_core(). It also hosts the Operations API — the REST endpoint that your UI widgets call directly when no SSE stream is needed.
App Anatomy
An app is a self-describing package. Its runtime composition, human documentation, machine-readable interfaces, configuration, storage ownership, tests, release metadata, and implementation journal must stay synchronized.
| Contract | Purpose |
|---|---|
entrypoint.py | Thin runtime composition root: app identity, decorators, defaults, surface wiring, and lifecycle hooks. |
README.md | Human overview of the product role, boundaries, surfaces, dataflow, and package map. |
AGENTS.md | Operational implementation contract for coding agents: read order, invariants, forbidden shortcuts, synchronization rules, and validation commands. It is not another product README. |
config/ | Complete non-secret descriptor template with safe defaults, plus a secret template containing placeholders only. |
interface/ | Human and machine-readable declarations for every exposed surface family. OpenAPI may have no HTTP paths while x-kdcube-surfaces declares MCP, widgets, named services, Data Bus handlers, or jobs. |
docs/storage/README.md | Ownership matrix distinguishing durable, read-through, cached, secret, provider-owned, and ephemeral state. |
tests/ | Manifest, interface, authorization, configuration, storage, and primary product-path checks. |
release.yaml and docs/journal/ | Release metadata and dated implementation decisions. Neither replaces stable documentation or runtime configuration. |
Only add conditional source folders such as ui/, agents/, tools/, surfaces/, events/, or integration modules when the app actually provides them. The illustration below shows one agent-oriented package, not a mandatory tree.
Minimal Entrypoint
from kdcube_ai_app.infra.plugin.bundle_loader import bundle_entrypoint, bundle_id
from kdcube_ai_app.apps.chat.sdk.solutions.chatbot.entrypoint import BaseEntrypoint
from langgraph.graph import StateGraph, START, END
from typing import Dict, Any
BUNDLE_ID = "my-bundle@1-0"
@bundle_entrypoint(name="My App", version="1.0.0")
@bundle_id(BUNDLE_ID)
class MyBundle(BaseEntrypoint):
def configuration_defaults(self) -> Dict[str, Any]:
return {
"role_models": {
"solver.react.v2.decision.v2.strong": {
"provider": "anthropic",
"model": "claude-sonnet-4-6"
}
}
}
async def on_bundle_load(self, **kwargs):
# Called ONCE per process when bundle first loads.
self.logger.info("App loaded!")
async def execute_core(self, state, thread_id, params):
graph = StateGraph(dict)
graph.add_node("run", self._run_node)
graph.add_edge(START, "run"); graph.add_edge("run", END)
return await graph.compile().ainvoke(state)
async def _run_node(self, state):
await self._comm.delta(text="Hello from my app!", index=0, marker="answer")
return {"final_answer": "Done", "followups": []}
BaseEntrypoint Lifecycle
| Method / Property | When Called | Typical Use |
|---|---|---|
configuration_defaults() | When effective app configuration is resolved | Declare code-owned defaults; the platform's configuration property deep-merges effective descriptor props over them |
on_bundle_load(**kwargs) | Once per process, per tenant/project | Build knowledge index, connect external services, warm caches |
on_apply_props(props) | Every effective-props application, after platform-reserved paths | Apply app-specific props to runtime objects; return True only when the model service must be rebuilt |
on_props_changed(...) | When effective app props changed | Reconcile long-lived side effects after prop refresh |
pre_run_hook(state) | Before each turn | Per-turn setup, state enrichment, request-local validation |
execute_core(state, thread_id, params) | Every turn | Build + invoke your LangGraph workflow |
post_run_hook(state, result) | After successful turn execution | Fast final bookkeeping after the main result |
on_turn_completed(...) | After completion, error, or cancellation | Cleanup that must run even when a turn fails |
handle_job(**kwargs) | When a background job is dispatched | Reusable dispatcher for @on_job and mixin-owned background work |
rebind_request_context(...) | On cached singleton reuse | Refresh request-bound state (comm, user, etc.) |
Runtime hooks are normal entrypoint methods, not manifest decorators unless explicitly documented as decorators. If an app uses SDK mixins, call super() from completion and job hooks so mixin cleanup and background dispatch still run.
App Lifecycle
An app goes through a well-defined lifecycle from discovery to shutdown. Understanding these phases helps you place initialization logic, manage state, and handle configuration changes correctly.
Discovery and Loading
App source can come from git or from a mounted local path. The platform resolves the configured app source, imports the module, and finds the class decorated with @bundle_entrypoint. The loader extracts interface metadata (decorators for widgets, APIs, message handler, and background job handler) and builds an internal interface manifest that the REST layer and processor use for routing.
Singleton Instance Model
By default each incoming turn or operation creates a fresh entrypoint instance. When the registry sets singleton=true, the platform caches and reuses one instance per loaded app spec in the current process worker.
self.comm, current actor, conversation/turn ids) are rebound per invocation via rebind_request_context() and must never be cached across requests. Durable state belongs in app storage, Redis, or app props — not on self. If an app needs instance-local filesystem state, it should resolve that location through the platform storage helper instead of creating ad hoc runtime folders next to app source code. Singleton reuse is keyed by the loaded app spec (path + module), not by tenant/project and not by bundle_id alone.
Initialization Hooks
| Hook | Frequency | Typical Use |
|---|---|---|
on_bundle_load(**kwargs) | Once per process, per tenant/project | Build indexes, warm caches, clone repos, prepare local read-only assets, trigger UI build |
on_apply_props(props) | Every props refresh on turn, REST, widget, MCP, and local-operation surfaces | Apply app-specific configuration after platform-owned role_models, embedding, and services.llm.custom paths |
on_props_changed(...) | When effective props changed for the active instance | Invalidate prop-derived caches, mark helpers dirty, reconcile side effects after live prop updates |
pre_run_hook(state) | Every invocation | Last-minute validation or reconciliation before execution |
execute_core(state, thread_id, params) | Every invocation | Main app logic (chat turn or operation handling) |
post_run_hook(state, result) | Every invocation | Final bookkeeping after execution completes |
on_turn_completed(...) | Every invocation exit path | Cleanup after success, error, or cancellation |
handle_job(**kwargs) | Background job dispatch | Handle mixin-owned or app-owned work_kind values after proc claims a job |
on_memory_reconciliation_request(request) | Before a memory reconciliation job is stored/enqueued | Validate or augment one request's memory reconciliation controls |
rebind_request_context(...) | Singleton reuse only | Refresh request-local handles on cached instance before the current call |
on_bundle_load() must be deterministic and idempotent. It runs before any request depends on the app and is the right place for bounded preparation work. Do not store request-local state there.
on_apply_props(props) is the public async seam for applying app-specific configuration to live runtime objects. It runs after platform-interpreted paths on every effective-props refresh. Do not override the internal synchronous _apply_bundle_props_overrides; doing so can silently disable platform model, embedding, or custom-provider application.
on_props_changed(...) is different: it runs after effective app props changed for the active instance. Use it for long-lived side effects such as prop-derived caches, sidecar wrapper state, or other runtime helpers that must track app props. Do not use it for request-local validation or heavy one-time install/build work.
on_memory_reconciliation_request(request) is available on memory-enabled entrypoints. Return None or a JSON-safe patch to merge into the request; return {"ok": false, "error": "...", "message": "..."} to reject the request before a job is created.
Portable App Call Context
bundle_call_context is the app-owned, request-scoped context room. The internal name remains for compatibility. Use it for JSON-safe metadata that must follow nested agents, tools, background handlers, and isolated runtimes without asking the model to pass those values as tool arguments.
from kdcube_ai_app.apps.chat.sdk.runtime.comm_ctx import (
bind_current_bundle_call_context_patch,
get_current_bundle_call_context,
update_current_bundle_call_context,
)
# Visible for the rest of the current invocation.
update_current_bundle_call_context({
"my_bundle": {"selected_mode": "lite"},
})
# Temporarily override one named agent role for one nested run.
with bind_current_bundle_call_context_patch({
"role_models": {
"my.named.agent": {
"provider": "anthropic",
"model": "claude-haiku-4-5-20251001",
},
},
}):
await self.run_named_agent(...)
current = get_current_bundle_call_context()
Read it from self.comm_context.bundle_call_context or get_current_bundle_call_context() in entrypoints, APIs, widget operations, and @on_job handlers. In in-process tools, bundle_tool_context.scope()["bundle_call_context"] is a tool-side reader for the same room. Isolated Docker/Fargate runtimes restore the same context through the runtime globals snapshot.
This context is not durable storage. It survives the current execution graph and child runtime boundaries; it does not automatically survive a later request. If a later background job needs the same decision, store it in the job payload/metadata or another durable app-owned location, then rebind it when that job runs. PORTABLE_SPEC_JSON is platform-built; app-owned per-call data belongs in the technically named bundle_call_context.
bundle_call_context.role_models is a reserved request-scoped overlay interpreted by the model router. Precedence is current call context first, then effective app props role_models, then platform defaults.
Hot-Reload on Config Change
When you update an app's ref in bundles.yaml and re-apply, the platform detects the configuration change via a content hash of the app directory. The updated app is loaded in the current process without a service restart. on_bundle_load() runs again for the new version, and subsequent requests are served by the refreshed instance.
For props-only live updates, the active app instance refreshes effective props on the next invocation, and on_props_changed(...) fires only if the effective props actually changed. Already-loaded singleton apps in the current worker also receive that hook on live bundles.props.update events.
Shutdown and Cleanup
The platform does not expose a dedicated shutdown hook. Because app state should live in external storage (Redis, S3, local shared storage), process termination is safe by design. Transient per-invocation state in OUT_DIR / workdir is scoped to the request and cleaned up automatically.
Storage Surfaces Across Phases
| Surface / Scope | Read / write API | Live authority today | Example | Export / ejection path |
|---|---|---|---|---|
| Platform/global props | read: get_settings()raw read: get_plain("...")write: none from normal app code | Deployment descriptors such as assembly.yaml and gateway.yaml; process-environment keys are generated runtime projections or implementation aliases, not the operator write surface | Ports, auth ids, storage backends, runtime path roots | Outside kdcube export; manage through deployment descriptors |
| Platform/global secrets | read: await get_secret("canonical.key")write: none from normal app code | Configured secrets provider; in local secrets-file mode this is secrets.yaml | Deployment-wide API keys and auth secrets | Outside kdcube export; manage through deployment secret workflows |
bundle_props / bundle_prop(...) | read: self.bundle_prop(...)write: await set_bundle_prop(...) | The configured descriptor provider; file mode persists to bundles.yaml. Redis is a derived runtime cache, never the authority. | App feature flags, cron config, model selection, UI config | Exported to bundles.yaml by kdcube export |
| App secrets | read: await get_secret("b:...")write: await set_bundle_secret(...) | Configured secrets provider; in local secrets-file mode this is bundles.secrets.yaml | App-scoped webhook secrets and shared API tokens | Exported to bundles.secrets.yaml when the provider/export flow can reconstruct app secrets |
| User app props | await get_user_prop(..., default=...), await get_user_props(), await set_user_prop(...), await delete_user_prop(...) | PostgreSQL <SCHEMA>.user_bundle_props | Per-user non-secret preferences and app state | Never exported to descriptors or app export |
| User app secrets | read: await get_secret("u:...")write/delete: await set_user_secret(...), await delete_user_secret(...) | Configured user-scoped secrets provider | Per-user secret material such as personal access tokens | Never exported to descriptors or app export |
| Redis KV cache | developer-defined read/write keys | Redis only | Lightweight distributed state, flags, small caches | No descriptor export |
BundleArtifactStorage | artifact storage APIs | S3 or localfs backend, depending on deployment | Persistent app artifacts addressed through the storage API | Outside descriptor export |
Shared local storage (BUNDLE_STORAGE_ROOT) | self.bundle_storage_root() or bundle_storage_dir(...) | Host-local or shared instance-visible storage | Large local caches, cloned repos, indexes, mutable local workspaces, cron state | Outside descriptor export |
OUT_DIR / workdir | filesystem read/write | Current invocation only | Transient turn files, generated artifacts | Ephemeral; not exported |
| Hosted conversation files | ret.artifact_type == "files" or host_files(...) | Conversation store plus current-turn file event metadata | User-visible downloads and attachments produced by tools or isolated execution | Current conversation artifact surface; not descriptor state |
Only deployment-scoped app state belongs to app descriptors and app export. Platform/global deployment state and all user-scoped state stay outside kdcube export.
In supervised Docker/Fargate execution, the trusted supervisor receives and materializes descriptor payloads before tool bootstrap. App tools therefore use the normal SDK helpers. The claim that generated code receives no descriptor files, provider secrets, or network applies to the reference Docker split executor; local subprocess, combined Docker, and Fargate have different physical boundaries. Set execution.runtime.descriptor_payload_scope: active_bundle when the supervisor should receive only the active app sections of bundles.yaml and bundles.secrets.yaml.
Reference docs: bundle-runtime-configuration-and-secrets-README.md, runtime-read-write-contract-README.md, how-to-configure-and-run-bundle-README.md.
await get_secret("b:...") resolves the current app from bound runtime context; await get_secret("u:...") additionally resolves the current user. Outside a bound request or app runtime, pass explicit scope or use a fully qualified deployment secret path. User prop values pass through asyncpg JSON/JSONB codecs as native Python values. Compatibility readers decode a second JSON layer only when a decoded string begins with { or [, preserving literal strings such as false and de-DE.
Node / TypeScript Backend Inside an App
If your application backend already exists in Node or TypeScript, the supported pattern is to keep the public KDCube app surface in Python and run the Node backend as an app-local sidecar.
That split is intentional:
- Python app shell owns decorators, auth, role gating, app props, app secrets, and the public API / widget / MCP / cron contract.
- Node backend owns internal domain logic behind a narrow route boundary.
my.bundle@1-0/
entrypoint.py
backend_src/
package.json
src/
bridge_app.ts
The Python side starts the sidecar through ensure_local_sidecar(...). Current runtime behavior:
- one sidecar instance per worker for the active loaded app spec and
tenant/projectscope - app code reload stops the sidecar and the next call starts it fresh
- props-only updates do not proactively restart the sidecar at publish time
- startup-config changes restart lazily on next use; live config can be pushed lazily through
POST /__kdcube/reconfigure
This lets you wrap an existing Node backend without replacing the Python app shell. The app remains the KDCube application unit; Node is one internal implementation part of that app.
Reference docs: bundle-node-backend-bridge-README.md and node-backend-sidecar-README.md.
Interface: In & Out
An app's interface is the union of the surface families it actually declares: operations or public APIs, MCP, named-service providers, widgets and main views, reactive conversation handling, external events, Data Bus handlers, cron jobs, background jobs, integrations, and artifact namespaces. The package's decorators, descriptor config, interface/README.md, OpenAPI/x-kdcube-surfaces, tests, and journal must agree.
The chat contract below applies only to conversation-capable apps. API-only, service-only, UI-only, and job-only apps do not receive a ChatTaskPayload and do not need a communicator.
📥 Conversation Input
A scheduled conversation turn arrives as a ChatTaskPayload (Pydantic). Key fields:
request.message— user's textrequest.chat_history— prior messagesrequest.payload— arbitrary JSON (for REST ops)actor— tenant_id, project_idrouting— conversation_id, turn_id, bundle_iduser— user_id, user_type, roles, timezonecontinuation— follow-up or steer type
📤 Conversation Streaming
A conversation-capable app streams through the Communicator. The client receives ChatEnvelope events in real time over SSE or Socket.IO.
- delta — streaming text chunks (thinking / answer)
- step — tool calls, status updates, timeline events
- complete — turn finished with final data
- error — propagate errors cleanly
- event — custom events (artifacts, reactions, etc.)
Using the Communicator
# Stream answer text
await self._comm.delta(text="chunk...", index=0, marker="answer")
# Announce a step
await self._comm.step(step="web_search", status="started", title="Searching the web...")
await self._comm.step(step="web_search", status="completed")
# Emit follow-up suggestions
await self._comm.followups(["Tell me more", "Show examples"])
# Final complete signal
await self._comm.complete(data={"answer": "..."})
Recording comm events and sending event batches
Apps can record selected post-firewall communicator envelopes into a bounded, scoped buffer and send the batch to a configured event sink. Recording is additive and scoped: a workflow, API handler, MCP endpoint, job handler, or tool can add a JSON-serializable scope without replacing the outer scope.
async with self.comm.recording(
EVENT_SELECTOR,
scope={"owner": "workflow", "bundle": bundle_id},
mode="replace",
max_events=500,
sink=event_sink,
send_on_exit=True,
):
await self.run_react(...)
Configured isolated child tool runtimes receive portable active scopes through COMM_SPEC. Child-added scopes are recorded in the child buffer, written to comm_recorded_events.json, merged by the host, and sent by the host sink. Sink callbacks are live Python objects and are not serialized into child runtimes.
Open the recording scope at the boundary that owns the invocation: @on_message workflows, @api methods, @mcp methods, and @on_job handlers can all record when a communicator is bound. @cron is normally headless, so use comm recording only when the cron path invokes a comm-bound flow; otherwise enqueue a job or persist durable operational facts directly.
Reference docs: bundle-event-recording-and-sinks-README.md, comm-recording-event-sinks-README.md.
REST Operations (for UI Widgets)
Your app exposes additional REST operations through the Operations API hosted by the Processor:
POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation}
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation}
The explicit bundle_id form is the preferred route. The processor resolves the target method from the app's decorator-discovered interface surface rather than assuming every operation is just workflow.run(). A default-app compatibility shortcut still exists at POST /api/integrations/bundles/{tenant}/{project}/operations/{operation}.
Runtime Availability And Visibility
Bundle availability and resource visibility are deployment-scoped bundle props. The whole bundle is gated through the canonical enabled.bundle prop resolved by the platform. APIs and widgets declare code defaults for user types and roles, and can declare prop paths that the Bundle Admin UI can override. Resource enabled flags are stored in bundle props/admin state; do not pass the removed enabled_config argument to @api or @mcp.
@bundle_entrypoint(
name="Ops",
version="1.0.0",
allowed_roles=("kdcube:role:viewer",),
allowed_roles_config="visibility.bundle.allowed_roles",
)
class OpsBundle(BaseEntrypoint):
@api(
alias="report",
user_types=("registered",),
user_types_config="visibility.api.report.user_types",
roles_config="visibility.api.report.roles",
)
async def report(self, **kwargs):
...
@ui_widget(
alias="admin",
icon={"lucide": "Settings"},
user_types=("privileged",),
roles_config="visibility.widget.admin.roles",
)
async def admin_widget(self, **kwargs):
...
@mcp(alias="automation", transport_config="mcp.automation.transport")
def automation_mcp(self, **kwargs):
...
@cron(alias="news-sync", expr_config="jobs.news_sync.cron")
async def news_sync(self) -> None:
...
The visibility dot paths are resolved against effective bundle props. Missing or invalid values fall back to the decorator defaults. Empty user-type or role selections are intentional overrides that mean no restriction for that selector. Cron jobs use expr_config; blank or disable values disable the job. MCP endpoint request authorization belongs to the bundle-served MCP app, not to user_types / roles on the @mcp decorator.
enabled.bundle is falsy, bundle operations, widgets, and MCP endpoints return 404, and proc skips scheduled jobs for that bundle.Because these values live in bundle props, they work with hot bundle reload and live props updates. This is the mechanism that lets Bundle Admin change bundle visibility, API/widget visibility selectors, resource enabled flags, and scheduled-job expressions without a redeploy.
Continuation Types
| Type | Description |
|---|---|
regular | Normal new message |
followup | User clicked a suggested follow-up |
steer | User is redirecting the ongoing turn |
Classic vs. Interactive Turn Execution
| Mode | Behavior |
|---|---|
| Classic turn execution | The turn runs to completion before the user can affect the next agent step. From the user perspective, input is effectively blocked until the turn finishes and emits its final result. |
| Interactive turn execution | While a turn is running, the user can still contribute to the same conversation. followup and steer are written into the conversation event lane. A live React turn can consume them immediately: followup stays on the current turn, while steer redirects the current turn at the next safe checkpoint. Idle-turn handling is controlled by the reactive-event admission path rather than by bundle UI code. |
followup and steer. A busy conversation accepts these into the shared event lane rather than dropping them. followup is a live “continue on this turn” input, while steer is a live “redirect this turn” signal. When React owns the turn it consumes these directly on the current timeline.Long ReAct turns may also emit chat_compaction transport events with semantic type chat.compaction. These mark context compaction start/completion while the turn continues; clients should render them as progress/activity items, not as final answers.
Storage
KDCube separates storage by ownership and lifecycle. The three primitives below are not interchangeable, and an app may use none, one, or several of them.
Cloud Storage (BundleArtifactStorage)
from kdcube_ai_app.apps.chat.sdk.storage.bundle_artifact_storage import BundleArtifactStorage
storage = BundleArtifactStorage(
tenant="my-tenant", project="my-project",
bundle_id="my-bundle@1-0",
storage_uri="s3://my-bucket" # or file:///data/bundle-storage
)
storage.write("reports/latest.json", data='{"count": 42}')
content = storage.read("reports/latest.json", as_text=True)
keys = storage.list("reports/")
App Filesystem Storage
# In entrypoint or workflow
root = self.bundle_storage_root() # pathlib.Path
index_path = root / "knowledge_index"
index_path.mkdir(exist_ok=True)
Path namespaced: {BUNDLE_STORAGE_ROOT}/{tenant}/{project}/{bundle_id}/. bundle_storage_root() is a filesystem path, never an S3 API. Local runtime uses local or mounted storage; multi-worker cloud deployments normally mount shared filesystem storage such as EFS when workers must see the same files. Use BundleArtifactStorage separately when the app needs the backend artifact API backed by S3 or localfs.
Document the owner of every dataset. For example, connected-account metadata may live in app filesystem storage while provider tokens stay in user-scoped secrets; delegated KDCube grants and sessions use server-side grant/session records. Provider-owned mail and Slack bytes remain provider-owned unless an explicit operation materializes them.
Redis Cache
# Low-level Redis client (aioredis)
await self.redis.set("my:key", "value", ex=3600)
val = await self.redis.get("my:key")
# KVCache wrapper
await self.kv_cache.set("user_prefs", {"theme": "dark"}, ttl=86400)
Workflow Orchestration
KDCube does not require one orchestration framework. An app can use LangGraph, LangChain, CrewAI-authored logic, the built-in KDCube ReAct agent, plain Python, or another runtime behind its app boundary. The example below uses LangGraph and BaseWorkflow.
ReAct and hosted/ported agents are sibling consumers of the shared Agent Harness. A run-to-completion LangGraph adapter can use harness workspace, resolver, timeline, and conversation-recorder contracts without inheriting ReAct's live mid-turn event folding or round protocol. In concurrent multiuser serving, keep graph objects turn-scoped: build the graph for the turn, reuse durable connections or checkpointers where appropriate, and release the graph when the turn ends.
A fresh ReAct runtime object is built for each accepted turn from app code, the administrator capability ceiling, conversation-scoped user selection, and durable timeline/workspace/memory/runtime/event-lane state. Construction is per turn; state is not.
from kdcube_ai_app.apps.chat.sdk.solutions.chatbot.base_workflow import BaseWorkflow
from kdcube_ai_app.apps.chat.sdk.runtime.tool_config import agent_tool_config_from_bundle_props
from kdcube_ai_app.apps.chat.sdk.runtime.skill_config import agent_skill_config_from_bundle_props
class MyWorkflow(BaseWorkflow):
def __init__(self, *args, bundle_props=None, **kwargs):
super().__init__(*args, bundle_props=bundle_props, **kwargs)
async def process(self, payload):
scratchpad = self.start_turn(payload)
try:
client_id = self.runtime_ctx.agent_id
tools = agent_tool_config_from_bundle_props(
self.bundle_props, client_id, bundle_root=self.bundle_root()
)
skills = agent_skill_config_from_bundle_props(
self.bundle_props, client_id, bundle_root=self.bundle_root()
)
tools, skills = await self.apply_user_agent_selection(tools, skills)
tools = await self.apply_delegated_tool_claims(tools)
react = self.build_react(
scratchpad=scratchpad,
mod_tools_spec=tools.tool_specs,
mcp_tools_spec=tools.mcp_tool_specs,
tools_runtime=tools.tool_runtime,
tool_traits=tools.tool_traits,
custom_skills_root=skills.custom_skills_root,
skills_visibility_agents_config=skills.agents_config,
)
result = await react.run()
self.finish_turn(scratchpad, ok=True)
return result
except Exception as e:
self.finish_turn(scratchpad, ok=False); raise
BaseWorkflow for the quickest path. It wires up ConvMemories, TurnStatus, ContextRAG, ApplicationHosting, and gives you build_react() which assembles the full ReAct agent with all tools and skills resolved.
build_react() automatically honors the current agent's react.<agent_id>.additional_instructions and appends that administrator customization last inside the shared hard-override envelope. Instruction profiles are declared under the same agent block; the picker carries IDs only, while the runtime resolves each selected profile to its body or blocks such as xlite:workspace_exec.
build_react() respects descriptor-backed runtime selection through ai.react.react_agent_version (projected as AI_REACT_AGENT_VERSION). The checked-in reference descriptor and code-level fallback currently select v2; choose v3 explicitly for its streamed-lane governance and optional multi-action path. If AI_REACT_AGENT_MULTI_ACTION=safe_fanout is enabled, accepted multi-action rounds are still executed sequentially unless a fully validated neutral call carries the supported detached early-execution traits. The base round cap resolves from app props, then assembly projection, then fallback 15.
BaseWorkflow Key Parameters
| Parameter | Type | Description |
|---|---|---|
conv_idx | ConvIndex | Conversation vector index for semantic search |
store | ConversationStore | File/S3-backed conversation storage |
comm | ChatCommunicator | Chat streaming channel for SSE or Socket.IO delivery |
model_service | ModelServiceBase | LLM registry / router |
ctx_client | ContextRAGClient | Conversation persistence/search client owned by sdk/solutions/conversation |
bundle_props | Dict | Bundle runtime configuration |
graph | GraphCtx | Optional knowledge graph context |
Simplest Agentic Workflow Pattern
A bundle can use any orchestration pattern — it is not required to use the ReAct agent. This is one common pattern that combines a Gate agent with the ReAct agent:
Tools System
| Tool | Namespace | Description |
|---|---|---|
web_search | web_tools | Neural web search with ranking (Brave / DuckDuckGo) |
web_fetch | web_tools | Fetch + parse web pages (readability-enabled) |
execute_code_python | exec_tools | Profile-selected Python execution: local subprocess containment, Docker combined compatibility, reference Docker split isolation, or external Fargate transport |
write_pdf | rendering_tools | Generate PDF from Markdown + table of contents |
write_png | rendering_tools | Render HTML/SVG to PNG image |
write_docx | rendering_tools | Generate DOCX from Markdown |
write_pptx | rendering_tools | Generate PPTX slide deck |
write_html | rendering_tools | Generate standalone HTML artifact |
fetch_ctx | ctx_tools | Fetch visible objects by qualified logical ref, including conv:ar:, conv:fi:, and external namespaces. |
read | react → react.read | Read visible logical refs such as conv:fi:, conv:ar:, conv:so:, conv:ws:, conv:su:, and conv:ev:. A bare fi: ref is invalid. |
write | react → react.write | Author current-turn content. Produced files belong under the turn's files/ role; editable project state belongs in the active workspace under git/projects/. |
pull | react → react.pull | Resolve and materialize visible owner refs such as mem:, task:, cnv:, or another registered namespace into the current turn. Returned conversation-owned file refs use conv:fi:. |
plan | react → react.plan | Create / update / close the current turn plan (shown in ANNOUNCE) |
patch | react → react.patch | Patch an existing current-turn text artifact. Unified diffs are supported; full replacement is used when the patch body is plain file content. Display line numbers in previews are never part of patch content. |
checkout | react → react.checkout | Construct or update editable project state under turn_<id>/git/projects/... from qualified conv:fi: refs. mode="replace" seeds from scratch; mode="overlay" imports selected historical files. |
memsearch | react → react.memsearch | Semantic search in past conversation turns |
rg | react → react.rg | Search materialized artifact files by name and text-like files by content regex; returns read-ready ranges for react.read. |
hide | react → react.hide | Replace timeline snippet with placeholder |
App-local tools use @kernel_function from Semantic Kernel and are connected to each agent through config.surfaces.as_consumer.agents.<agent_id>.tools. The app configuration is the agent's inventory ceiling: a user may narrow it, but cannot add a tool the app did not grant. Tool results should use the common {ok, error, ret} envelope. When a tool produces user-visible files, ret must use the strict file artifact protocol.
Tool calls and event sources may share block-production and timeline machinery, but they are opposite directions and are not interchangeable. Tool direction is model → fence → provider → result → timeline. Event direction is provider/event lane → block production → timeline → model. Event-source configuration binds owner discovery, block production, and pull policy; it does not transport events and does not create a model-callable tool.
# tools/my_tools.py
from typing import Annotated
import semantic_kernel as sk
from semantic_kernel.functions import kernel_function
class MyTools:
@kernel_function(name="search", description="Search product catalog")
async def search(self,
query: Annotated[str, "Search query"],
limit: Annotated[int, "Max results"] = 5
) -> str:
# your logic here
return "results..."
# bundles.yaml (or the app's config template)
config:
surfaces:
as_consumer:
agents:
main:
tools:
- id: web
kind: python
module: kdcube_ai_app.apps.chat.sdk.tools.web_tools
alias: web_tools
allowed: [web_search]
- id: product
kind: python
ref: tools/my_tools.py
alias: my_tools
discovery: semantic_kernel
allowed: [search]
tool_traits:
search:
strategy: [exploration]
File-producing tools have two supported paths. They can return local files declaratively:
{
"ok": true,
"error": null,
"ret": {
"artifact_type": "files",
"files": [
{
"path": "report.pdf",
"filename": "report.pdf",
"mime": "application/pdf",
"visibility": "external",
"description": "Generated report"
}
]
}
}
Or a trusted app/catalog tool can host files itself with bundle_tool_context.host_files(...) and return the already-hosted rows. In an active conversation, both paths emit chat.files with object refs; the browser resolves bytes at click time under the user's session, and the model sees a delivery note rather than a signed URL. A turn-less MCP caller has no chat lane, so the original URL-bearing result remains unchanged. Generated executor code should call a catalog tool through agent_io_tools.tool_call(...) when it needs hosted files; host_files(...) is for trusted tool code.
host_files(...) works only after the SDK has prepared the tool runtime: active ToolSubsystem, hosting service, tenant, project, user id, conversation id, turn id, conversation storage, and output directory. Normal React workflows prepare this through BaseWorkflow.build_react(...); isolated execution prepares it through bootstrap_bind_all(...). If that context is missing, the helper raises a runtime error instead of creating an unscoped artifact.
Custom domain artifact refs can also be exposed through namespace rehosters. A loaded tool or event module can register @artifact_namespace_rehoster(namespace="ext"); then react.pull(paths=["ext:..."]) resolves that opaque domain ref and returns a materialized conv:fi: ref for later react.read, react.rg, or generated-code use.
See custom-tools-README.md and the reference app's per-agent tool configuration.
Consumed MCP services and the tools visible to each agent both live in the app consumer surface. A tool entry's server_id must match a server under surfaces.as_consumer.mcp.services.
# bundles.yaml — connection plus per-agent inventory
config:
surfaces:
as_consumer:
mcp:
services:
mcpServers:
knowledge:
transport: streamable-http
url: https://mcp.example.com
auth:
type: bearer
secret: b:mcp.knowledge.token
agents:
main:
tools:
- id: knowledge
kind: mcp
server_id: knowledge
alias: knowledge
allowed: ["*"]
tool_traits:
"*":
strategy: [exploration]
For a hosted agent acting for the signed-in user, configure the connection with delegated: true, claims in scopes, and a resource that byte-matches the delegated-resource catalog ID; url remains the concrete endpoint to dial. The runtime reuses the agent's server-side grant bearer per turn and drops a consent-pending connection before contacting the server. The agent identity is kdcube-agent:<app>:<agent>, so sibling agents never share grants.
See mcp-README.md and consume-mcp-service-README.md for transports, authentication, and delegated hosted-agent connections. KDCube negotiates the current MCP 2026-07-28 flow and can fall back to the legacy 2025-11-25 protocol for older peers.
Named-Service Tools
Named-service tools are the preferred way for agents to work with app-owned object systems. Instead of adding separate direct tools for every subsystem, a provider exposes a namespace with about/schema/search/action/upsert/delete operations and optional block policies. Consumers configure which namespaces and operations are allowed.
Provider-backed account authorization has two live boundaries. Delegated to KDCube establishes or upgrades the user's provider account; Delegated by KDCube grants one hosted agent access to the exact account and operation. Capability is checked first so connect or claim-upgrade demands do not misroute into the agent-grant flow. Missing consent is raised at the concrete operation attempt, and revoking either boundary stops the tool.
Provider metadata
provider.about, object.schema, search filters, guarded use cases, connected-account requirements, and the required presentation layer form one self-description for two readers: agents work the realm; users understand, narrow, and consent from its service card.
Consumer policy
The tool connection declares the configuration ceiling for allowed namespaces and operations. Strategy and execution traits govern ordering and scheduling, not authorization. Before an action, the agent reads the provider's schema and uses only payload keys that contract declares.
Object projection
react.pull materializes object refs, react.read uses block production policies, and timeline rendering can call block.render for provider-owned blocks.
See Object Ecosystem & Ontologic Contracts and ReAct named-service flow.
Artifact Path Families
Conversation-owned refs use the strict form conv:<family>:<body>. The conv: prefix is an owner namespace; a body segment such as conv_123 is a physical conversation identifier and must not be rewritten. ReAct tools resolve these refs under the current request identity before any bytes are materialized.
| Ref or path | Meaning | Example |
|---|---|---|
conv:fi: | Conversation-owned file bytes or a materialized workspace file. | conv:fi:conv_123.turn_456.files/report.pdf |
conv:ar:, conv:tc:, conv:so: | Replica records, tool records, and source-pool rows. | conv:ar:conv_123.turn_456.assistant.completion |
conv:ws:, conv:su:, conv:ev: | Working summaries, summary records, and accepted timeline events. | conv:ev:conv_123.turn_456.event_7 |
turn_<id>/git/projects/... | Editable project/workspace state. | turn_456/git/projects/app/src/main.py |
turn_<id>/files/... | Produced files and deliverables. | turn_456/files/report.pdf |
turn_<id>/git/snapshots/... | Canvas, wizard, story, or workflow snapshots. | turn_456/git/snapshots/current.yaml |
attachments/..., external/... | Current-turn uploads and pulled or rehosted external evidence. | turn_456/external/vendor/evidence.json |
mem:, task:, cnv:, other owners | External owner refs. react.pull resolves authorized content and rehosts exact bytes as conv:fi:. | task:shipment:42 |
Reusable SDK Integrations
SDK integrations are product-neutral building blocks that apps import when they need external protocol support. They keep provider mechanics and transport details in the SDK while the app keeps user policy, routing, and workflow decisions.
Connection Hub
Connected provider accounts, per-agent Delegated-by-KDCube grants, external OAuth clients, manual automation access, authority providers, demand-driven consent, and server-side credential storage.
Email Integration
Reusable account store, Gmail OAuth/API access, iCloud IMAP/SMTP, attachment materialization, delivery formatting, Email MCP runs, and Claude Code email processing.
Telegram Integration
Telegram Bot API rendering, webhook update normalization, attachment hydration, progress streaming, Mini App auth, chat submitter helpers, and signed downloads.
Google Workspace
Account-scoped Gmail, Sheets, and Docs operations through the named-services boundary with provider consent and per-caller operation grants.
Integration Boundary
Apps own product policy; SDK integrations own reusable mechanics. Use thin app adapters to connect user resolution, storage roots, roles, and workflows.
See the dedicated SDK Integrations page for the current reusable integration package surface.
Skills System
Skills are reusable instruction sets that give agents specialized capabilities. A skill bundles a natural-language instruction (SKILL.md), tool references, and source references.
The skill registry is broader than a bundle's own skills/ folder. For each agent consumer, the runtime resolves core SDK skills, SDK solution skills, and bundle-local skills, then applies the bundle's AGENTS_CONFIG and the current tool catalog before showing the final list to the agent.
Built-in Platform Skills
| Skill ID | Namespace | Description |
|---|---|---|
url-gen | public | Form clean human-facing source URLs for fetch tools; not a file-delivery mechanism |
pdf-press | public | PDF generation and manipulation |
docx-press | public | DOCX document generation |
pptx-press | public | PPTX presentation generation |
png-press | public | PNG image rendering from HTML/SVG |
svg-press | public | SVG illustration and diagram production |
mermaid | public | Mermaid diagram generation |
link-evidence | internal | Citation and evidence linking |
sources-section | internal | Automatic sources section generation |
SDK solution skills, such as task-focused skills, are discovered by the same registry and filtered by the same AGENTS_CONFIG and required-tool rules. They do not need to live in the bundle's skills/ folder.
Custom Bundle Skill
# skills/product/kdcube/SKILL.md
You are an expert in our product catalog.
When asked about products, use the `product_search` tool to find relevant items.
Always include pricing and availability.
# skills/product/kdcube/tools.yaml
tools:
- id: product_search
role: search
why: Search the product catalog
required: true
# skills_descriptor.py
AGENTS_CONFIG = {
"solver.react.v2.decision.v2.strong": {
"enabled": ["product.kdcube", "public.pdf-press", "public.url-gen"],
"disabled": []
}
}
Creating a SKILL.md File
Each skill lives in its own folder under the bundle's skills/ directory. The required file is SKILL.md (or skill.yml), which contains YAML front-matter and a natural-language instruction body.
# skills/product/kdcube/SKILL.md
---
name: "Product Catalog Expert"
id: "kdcube"
namespace: "product"
description: "Search and present product information"
version: "1.0"
tags: ["catalog", "products"]
when_to_use: "When the user asks about products, pricing, or availability"
imports: []
agent_disclosure: "normal"
---
You are an expert in the product catalog.
When asked about products, use the `product_search` tool.
Always include pricing and availability in your response.
Optional companion files in the same folder:
compact.md— a shorter instruction variant for context-constrained agentssources.yaml— sources injected intosources_poolwhen the skill loads (exposed through qualifiedconv:so:refs)tools.yaml— recommended or required tools for the skill, used by planners and runtime eligibility checks
Skill Visibility Configuration
The skills_descriptor.py file controls which skills are visible to which agent consumers. It exposes two key variables:
# skills_descriptor.py
import pathlib
BUNDLE_ROOT = pathlib.Path(__file__).resolve().parent
# Points to the skills folder layout: <root>/<namespace>/<skill_id>/SKILL.md
CUSTOM_SKILLS_ROOT = BUNDLE_ROOT / "skills"
AGENTS_CONFIG = {
# Allow-list for the ReAct decision agent
"solver.react.v2.decision.v2.strong": {
"enabled": ["product.kdcube"]
},
"solver.react.v2.decision.v2.regular": {
"enabled": ["product.kdcube", "public.*"]
},
# Deny-list for a generator agent
"answer.generator.strong": {
"disabled": ["public.*"]
},
}
| Rule | Behavior |
|---|---|
enabled: [...] | Allow-list: only listed skills are visible to that consumer |
disabled: [...] | Deny-list: listed skills are hidden from that consumer |
| Wildcards | Supported in both lists ("public.*", "public.docx-*", "*") |
| Missing consumer entry | No descriptor filtering — registered skills may still be removed by required-tool eligibility |
Runtime Skill Eligibility
AGENTS_CONFIG is only the first filter. ReAct also compares each skill's required tool references with the active tool catalog for the current agent and turn. If a skill declares a required tool that is not available, the skill is omitted from the visible catalog, SK1/SK2 short ids, imports, and react.read("sk:...") for that runtime context.
# skills/public/pdf-press/tools.yaml
tools:
- id: rendering_tools.write_pdf
role: render pdf
required: true
Use required: true for subsystem skills that would confuse the agent without the matching tools. For example, PDF/DOCX/PPTX skills depend on rendering tools, and task skills depend on task tools. If those tools are not registered for a bundle, the corresponding skills disappear automatically without adding deny-list entries.
Hidden Operational Skills
Some skills are useful as imported operational guidance but should not be advertised when a user asks what the agent can do. Add agent_disclosure: hidden in the skill front matter for that case.
---
name: memory-journal
namespace: product
description: Operational guidance for durable memory tools.
agent_disclosure: hidden
imports: []
---
A hidden-disclosure skill remains loadable by exact id or import when it is otherwise eligible, but it is excluded from visible catalogs and short-id mappings. If it is explicitly loaded, the active skill block uses a redacted heading and a non-disclosure rule. This is prompt-disclosure control, not authorization; use AGENTS_CONFIG and tool gating for actual availability.
If a parent skill imports an optional subsystem skill, keep the subsystem-specific instructions inside the imported skill. The registry can skip an ineligible import, but it cannot rewrite arbitrary subsystem instructions that were duplicated in the parent skill body.
Skill Loading and Resolution
The runtime auto-detects <bundle_root>/skills as the custom skills root when CUSTOM_SKILLS_ROOT is not set. Skills are loaded into the skill registry and resolved by fully qualified id (namespace.skill_id). Consumer agents reference eligible visible skills via short-id tokens (SKx) or explicit sk:<id> references. When a skill is loaded with react.read("sk:<skill>"), its sources are merged into sources_pool and citation tokens are rewritten to match.
CUSTOM_SKILLS_ROOT = None does not reliably disable bundle-local skills — the runtime falls back to auto-detection. To truly disable them, remove the skills/ folder or set CUSTOM_SKILLS_ROOT to a non-existent path.
Best Practices for Skill Authoring
- Keep
SKILL.mdinstructions focused — one skill per capability domain - Use
when_to_usefront-matter to help the agent decide when to activate the skill - Include
tools.yamlto pair skills with the tools they need, and mark hard dependencies withrequired: true - Use
AGENTS_CONFIGto restrict skill visibility rather than deleting skill files — this keeps skills available for future consumers - Use
agent_disclosure: hiddenonly for loadable operational guidance that must not be advertised in self-description - Test visibility filtering per consumer: a skill hidden from
solver.react.v2.decision.v2.strongis still visible to unfiltered consumers
Widgets, APIs, MCP & Custom UI
Apps expose a decorator-discovered interface. Widgets, REST APIs, app-served MCP endpoints, chat message handlers, named-service providers, and background job handlers are separate surfaces: widgets are UI entrypoints intended for the platform shell, APIs are programmatic operations reachable under the integrations routes, @mcp(...) exposes an MCP-native surface for external MCP clients, and @on_job receives ready work claimed by proc. An app may also define a custom main view and a dedicated message entrypoint.
Buildable React/Vite Widgets
New widget apps should be source folders declared per alias under ui.widgets. The descriptor should pass the loader output destination through the standard placeholder, and the widget build config must consume it as an environment value:
ui:
widgets:
task_memo_webapp:
enabled: true
src_folder: widgets/task_memo_webapp
build_command: npm install --no-package-lock && OUTDIR=<VI_BUILD_DEST_ABSOLUTE_PATH> npm run build
task_webapp:
enabled: true
src_folder: widgets/task_webapp
build_command: npm install --no-package-lock && OUTDIR=<VI_BUILD_DEST_ABSOLUTE_PATH> npm run build
For Vite widgets, configure output from process.env.OUTDIR and use relative assets. Do not pass the destination path as vite build <path>. ui.widgets is the build/source contract only; the runtime widget surface is still declared by a matching @ui_widget(alias="...") method. A configured folder without a decorator is not a visible widget.
export default defineConfig({
base: './',
build: {
outDir: process.env.OUTDIR || 'dist',
emptyOutDir: true,
},
})
If runtime logs show vite build /.../.ui.build.tmp... or Vite reports UNRESOLVED_ENTRY for .ui.build.tmp.../index.html, the output directory leaked into Vite as a positional project/root argument. Fix the widget build contract or update to a platform runner that treats <VI_BUILD_DEST_ABSOLUTE_PATH> as an environment value. Do not manually copy built files into bundle storage.
App Interface Decorators
| Decorator | Attaches To | Purpose | Key args |
|---|---|---|---|
@bundle_entrypoint(...) | class | Marks the app entrypoint for loader discovery | name, version, priority, allowed_roles, allowed_roles_config |
@bundle_id(...) | class | Declares the canonical internal app id from code | id |
@ui_widget(...) | method | Declares a UI widget discoverable under the widgets endpoints | icon, alias, user_types, user_types_config, roles, roles_config |
@api(...) | method | Declares an app API method under /operations/{operation} or /public/{operation} | method=POST|GET, alias, route, user_types, user_types_config, roles, roles_config, public_auth, csrf |
@mcp(...) | method | Declares an app-served MCP endpoint under /mcp/{alias} or /public/mcp/{alias} | alias, route, transport |
@ui_main | method | Marks the app's main UI entrypoint returned in the interface manifest | no args |
@on_reactive_event | method | Declares the canonical chat/agent turn ingress for an accepted external-event batch | one per entrypoint |
@on_job | method | Marks the async ready-job handler called by proc after claiming a background job stream item | no args; method receives the job envelope and dispatch kwargs |
@cron(...) | method | Declares scheduled app work reconciled by the proc-owned scheduler | alias, cron_expression, expr_config, timezone, tz_config, span |
@data_bus_handler(...) | method | Declares a handler for durable app-owned Data Bus messages | subject, partition_by, ordering, idempotency, user_types, roles |
@public_content(...) | method | Declares a public discoverable-content alias: the platform serves crawlable pages, JSON-LD, and a per-alias sitemap from the app's content registry | alias, schema_type |
App identity can come from the registry entry or be declared explicitly with @bundle_id("my.bundle@version"). The loader turns discovered interface decorators into the app's HTTP, UI, conversation, and job contract.
For proc-owned API and widget routes, user_types use the threshold anonymous < registered < paid < privileged, while roles names raw external roles such as kdcube:role:super-admin. If both are declared, both must pass. Declared visibility is the source of additional role requirements; a normal operation with no declared visibility remains available to authenticated registered users. Admin-configurable variants live in app props. Public APIs separately require an explicit public_auth mode.
MCP has its own boundary. With surfaces.as_provider.mcp.<alias>.auth.mode: managed, proc applies the Connection Hub delegated-credential guard before dispatch. Otherwise the app's MCP subapp owns request authentication and authorization. @on_job is not URL-addressable: proc invokes the async handler only after claiming admitted background work. SDK mixins should receive the job first through await super().handle_job(**kwargs).
Example: Declare widget, API, MCP, and UI entrypoints
from fastapi import HTTPException, Request
from kdcube_ai_app.apps.chat.sdk.config import get_secret
from kdcube_ai_app.infra.plugin.bundle_loader import bundle_entrypoint, bundle_id, api, mcp, ui_widget, ui_main, on_message
@bundle_entrypoint(name="my-bundle", version="1.0.0", priority=100)
@bundle_id("my.bundle@1.0.0")
class MyBundle(BaseEntrypointWithEconomics):
@ui_widget(icon={"tailwind": "heroicons-outline:swatch"}, alias="dashboard", user_types=("registered", "privileged"))
def dashboard_widget(self, **kwargs):
return ["<div id='root'></div>"]
@api(alias="refresh_dashboard", method="POST", user_types=("registered", "privileged"), csrf=True)
def refresh_dashboard(self, **kwargs):
return {"ok": True}
@api(alias="telegram_webhook", method="POST", route="public", public_auth="bundle")
async def telegram_webhook(self, request: Request, **kwargs):
header_name = self.bundle_prop("telegram.webhook.auth.header_name", "X-Telegram-Bot-Api-Secret-Token")
expected_token = await get_secret("b:telegram.webhook.auth.shared_token")
if request.headers.get(header_name) != expected_token:
raise HTTPException(status_code=401, detail=f"Missing or invalid {header_name}")
return {"ok": True}
@mcp(alias="tools", route="operations", transport="streamable-http")
async def tools_mcp(self, request: Request):
from mcp.server.fastmcp import FastMCP
header_name = self.bundle_prop("mcp.inbound.auth.header_name", "X-App-MCP-Token")
expected_token = await get_secret("b:mcp.inbound.auth.shared_token")
if request.headers.get(header_name) != expected_token:
raise HTTPException(status_code=401, detail=f"Missing or invalid {header_name}")
server = FastMCP("my-bundle")
@server.tool()
def ping() -> str:
return "pong"
return server
@ui_main
def main_view(self, **kwargs):
return ["<div id='app'></div>"]
@on_message
async def run_message(self, **kwargs):
return await self.run(**kwargs)
public_auth="bundle" when the app, not proc, must verify the inbound request. Keep the client-facing header name in app config, the verification token in app secrets, and validate the request before doing work.auth.mode: managed when Connection Hub should validate a delegated bearer credential and tool grants. Use app-owned mode for custom headers, API keys, signatures, or JWT schemes, and validate them inside the MCP provider before returning the subapp.Integrations HTTP Surface
| Endpoint | Purpose | Notes |
|---|---|---|
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id} | Return the app interface manifest | Includes ui_widgets, api_endpoints, mcp_endpoints, ui_main, on_message |
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/widgets | List visible widgets for the current user | Returns alias, icon, user_types, roles |
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/widgets/{widget} | Render or fetch one widget | Resolved via @ui_widget |
POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation} | Call an app API operation | Resolved via @api; preferred explicit form |
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation}/csrf | Mint an operation-bound CSRF token | For cookie-authenticated POST operations that opt into csrf: true; returns csrf_required and a 10-minute single-use token |
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation} | GET variant for idempotent operations and static-like API reads | Resolved via the same interface manifest |
POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/{operation} | Call a public app API operation | Resolved only via @api(route="public", ...); public methods declare public_auth; public_auth="bundle" is the compatibility name for app-owned verification |
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/{operation} | GET variant for public idempotent app APIs | Supports the same route-scoped manifest contract |
GET|POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/mcp/{alias} | Call an app-served MCP endpoint | Resolved via @mcp(route="operations", ...); managed mode applies delegated-credential checks, while non-managed mode delegates authentication to the app |
GET|POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/mcp/{alias} | Call a public app-served MCP endpoint | Resolved via @mcp(route="public", ...); the route family does not replace the selected managed or app-owned auth policy |
POST /api/integrations/bundles/{tenant}/{project}/operations/{operation} | Default-app shortcut | Compatibility path when bundle_id is omitted |
GET /api/integrations/static/{tenant}/{project}/{bundle_id}/{path} | Serve app-scoped static assets | Useful for app-shipped frontend assets and media |
route="public" only for intentionally public endpoints and declare public_auth: "none", proc-side header_secret, or the compatibility value "bundle" for app-owned verification.csrf: true first fetches .../operations/{operation}/csrf, then sends the returned value as X-KDCube-CSRF-Token. The token is bound to subject, tenant, project, app, operation, and method; it is single-use and fails closed if the shared Redis backend is unavailable.How to configure an app-owned public API hook
The app defines the non-secret client contract in app props, stores verification material in app secrets, and checks the incoming request itself.
Server-side configuration
# bundles.yaml
bundles:
version: "1"
items:
- id: "partner.tools@1-0"
config:
telegram:
webhook:
auth:
header_name: "X-Telegram-Bot-Api-Secret-Token"
# bundles.secrets.yaml
bundles:
version: "1"
items:
- id: "partner.tools@1-0"
secrets:
telegram:
webhook:
auth:
shared_token: "replace-in-real-deployment"
What to share with the hook caller
- the public route:
/api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/telegram_webhook - the header name from app props, for example
X-Telegram-Bot-Api-Secret-Token - the shared token provisioned in app secrets
App-authenticated public API client call
curl -X POST \
"http://localhost:5173/api/integrations/bundles/<tenant>/<project>/<bundle_id>/public/telegram_webhook" \
-H "X-Telegram-Bot-Api-Secret-Token: <shared-token>" \
-H "Content-Type: application/json" \
-d '{"update_id":1}'
App-owned MCP authentication example
This example uses app-owned header verification. For delegated automation, configure surfaces.as_provider.mcp.<alias>.auth.mode: managed instead; proc then resolves the bearer handle to its server-side grant record before dispatch.
Server-side configuration
# bundles.yaml
bundles:
version: "1"
items:
- id: "workspace@2026-03-31-13-36"
config:
surfaces:
as_provider:
mcp:
preferences_tools:
auth:
mode: bundle
header_name: "X-Workspace-Preferences-MCP-Token"
# bundles.secrets.yaml
bundles:
version: "1"
items:
- id: "workspace@2026-03-31-13-36"
secrets:
surfaces:
as_provider:
mcp:
preferences_tools:
auth:
shared_token: "<rotate-me>"
App code
@mcp(alias="preferences_tools", route="operations", transport="streamable-http")
async def preferences_tools_mcp(self, request: Request, **kwargs):
header_name = self.bundle_prop(
"surfaces.as_provider.mcp.preferences_tools.auth.header_name",
"X-Workspace-Preferences-MCP-Token",
)
expected_token = await get_secret("b:surfaces.as_provider.mcp.preferences_tools.auth.shared_token")
if request.headers.get(header_name) != expected_token:
raise HTTPException(status_code=401, detail=f"Missing or invalid {header_name}")
return build_preferences_mcp_app(...)
What to share with the MCP client
- the MCP route:
/api/integrations/bundles/{tenant}/{project}/{bundle_id}/mcp/{alias} - the header name from app props, for example
X-Workspace-Preferences-MCP-Token - the token provisioned in app secrets
Authenticated MCP client call
curl -X POST \
"http://localhost:5173/api/integrations/bundles/demo-tenant/demo-project/workspace@2026-03-31-13-36/mcp/preferences_tools" \
-H "X-Workspace-Preferences-MCP-Token: <shared-token>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"1","method":"tools/list"}'
Public MCP client call
curl -X POST \
"http://localhost:5173/api/integrations/bundles/demo-tenant/demo-project/my.bundle/public/mcp/public_tools" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"1","method":"tools/list"}'
route="operations" and route="public" select only the URL family. Authentication comes from the surface policy: managed delegated credentials, app-owned verification, or an intentionally open provider.Interface Manifest Discovery
Decorators on the app class are the source of truth for its HTTP, UI, conversation, and job contract. At load time the loader scans the entrypoint class and builds the technically named BundleInterfaceManifest:
# Internal manifest shape (built automatically by the loader)
BundleInterfaceManifest(
bundle_id="workspace@2026-03-31-13-36",
ui_widgets=(UIWidgetSpec(alias="preferences", icon={...}, user_types=(...), roles=(...)),),
api_endpoints=(APIEndpointSpec(alias="preferences_exec_report", http_method="POST", route="operations", user_types=(...), roles=(...)),),
mcp_endpoints=(MCPEndpointSpec(alias="tools", route="operations", transport="streamable-http"),),
ui_main=UIMainSpec(method_name="main_ui"),
on_message=OnMessageSpec(method_name="run"),
)
The manifest is returned by GET /api/integrations/bundles/{tenant}/{project}/{bundle_id} and drives all route resolution. Only decorated methods are remotely callable — there is no same-name fallback for undecorated methods.
Consumed MCP Tools vs App-Served MCP
These are different surfaces and they solve opposite directions of integration.
| Use case | How you declare it | What it does | Where it is live |
|---|---|---|---|
| Use an external MCP server inside an agent | config.surfaces.as_consumer.mcp.services plus a kind: mcp entry under that agent's tools | Adds only the configured server and allowed tools to that agent's inventory | Inside agent/tool execution rounds |
| Expose an MCP-native surface from your app | @mcp(...) on an entrypoint method | Mounts a FastMCP or MCP-ready ASGI app through proc under /mcp/{alias} or /public/mcp/{alias} | At the integrations HTTP layer, callable by external MCP clients |
Widget UI Model
Widget methods (@ui_widget) declare app UI surfaces. Source-folder widgets are built and served by KDCube; legacy widgets may return small HTML fragments directly. The widget fetch endpoint resolves the alias, checks user_types and raw roles visibility, and invokes the method. user_types use the same threshold rule here: anonymous < registered < paid < privileged. If the same widget must also be callable through the operations route (for legacy clients), decorate it with both @ui_widget and @api:
@api(alias="task-board", route="operations", user_types=("registered",))
@ui_widget(alias="task-board", icon={"tailwind": "heroicons-outline:check-badge"}, user_types=("registered",))
def task_board(self, **kwargs):
return ["<div id='root'></div>"]
Widget and main UI browser code should follow the same runtime config bridge as the working platform widgets. First try the platform config endpoint GET /api/cp-frontend-config; if it returns usable config, do not wait for the parent frame. If the endpoint is unavailable or the host app owns the config, fall back to postMessage: send CONFIG_REQUEST to the parent frame and accept both CONN_RESPONSE and CONFIG_RESPONSE. Build app operation URLs from baseUrl, defaultTenant, defaultProject, and defaultAppBundleId. Do not infer app identity from a source-folder name or an internal static URL. Widget load should stay read-only by default; use an explicit in-widget action when the widget needs a syncing operation. For platform widgets, the preferred POST /operations/{alias} body shape is { "data": { ... } }; proc also accepts a raw JSON object body and treats it as data for webhook-style integrations. Clients should unwrap the returned {alias} field from the integrations response envelope.
When embedded in an iframe outside the platform shell, app UIs should also report their own size to the parent after mount and after layout changes: window.parent.postMessage({ type: "kdcube-resize", height: document.documentElement.scrollHeight, width: document.documentElement.scrollWidth }, "*"). The parent must still validate the message origin before applying the resize.
Use the SDK reference page for the exact widget/runtime config pattern and example: bundle-widget-integration-README.md.
Static File Serving for Custom UIs
Apps that ship a custom frontend (typically a Vite/React SPA in ui/main/ for the main view or ui/widgets/<alias>/ for widgets) have their built assets served from a dedicated static route:
GET /api/integrations/static/{tenant}/{project}/{bundle_id}/{path}
The endpoint serves files from the app's stable filesystem-backed bundle_storage_root()/ui/ subtree. Missing paths fall back to index.html for client-side routing. A <base> tag is injected into index.html so relative assets resolve correctly.
node_modules live in worker-local temporary storage; shared app storage holds only build outputs, signatures, temporary output, and cross-worker locks. npm/Vite runs in a dedicated process group that is terminated and reaped on timeout or explicit administrative cancellation.
One worker heartbeats the shared lock while building, publishes artifacts atomically, records the signature, and releases the lock before potentially expensive local cleanup. If that worker crashes, the heartbeat stops and lock TTL lets another worker retry. A request-time build fallback is supported when no current artifact exists. Bundle Admin's effective-props read is deliberately non-lifecycle: inspecting props never loads, evicts, cancels, or restarts an app or UI build and never calls on_bundle_load().
Application-Hosted Websites
An app can declare config.ui.main_view.site to expose its complete built main-view tree as a website. Site declarations are validated into a versioned ApplicationSiteCatalog; Redis distributes atomic generations, while each proc performs request-time host or alias lookup from an immutable in-memory snapshot. Direct aliases use /sites/{alias}. A CDN can preserve the viewer hostname and rewrite clean paths to /api/integrations/site-root/{path}, but it does not own the catalog.
This is different from @public_content: a site serves an app's files, directory indexes, and SPA fallback; public content serves indexed records, metadata, JSON-LD, and sitemaps.
Public Discoverable Content (SEO Pages & Sitemap)
Widgets and custom UIs are client-rendered — a crawler that fetches them sees an SPA shell. When an app publishes content that should be discoverable (articles, docs, catalog entries), it declares a public content alias with @public_content(alias=..., schema_type=...) and enables it in config. The app publishes, updates, and retracts items through the SDK registry (kdcube_ai_app.apps.chat.sdk.pub); the platform then serves the full discoverability layer with no further app code — crawlable HTML pages with real title/meta/body (verifiable with curl, no JS), rel=canonical + Open Graph/Twitter metadata, JSON-LD (declared @type plus BreadcrumbList), a per-alias sitemap.xml with accurate lastmod, and 410 Gone after retraction:
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/__content__
→ machine-readable sitemap descriptor list (host federation)
GET .../public/__content__/{alias}/sitemap.xml
→ the per-alias sitemap (published items only)
GET .../public/__content__/{alias}/{slug...}
→ crawlable item page (200) · 410 retracted · 404 unknown
Exposure is explicit: the alias must be enabled in the app's public_content.<alias> config block, and each item carries a published/retracted state — there are no per-user audience selectors on this surface. canonical_base in the same block decouples the canonical URL from the serving route: the operator maps a clean prefix at the CDN/proxy (a rewrite, never a redirect — a URL answering 3xx cannot be a canonical), and page canonicals, JSON-LD, and sitemap entries all use it. The widget URL stays a widget shell; the crawlable page is a separate platform-rendered artifact.
singleton: true: every crawler request resolves the app instance for the declaration/config gate (no app operation code runs on the serving path), so a non-singleton app would construct a fresh entrypoint per crawled page.
Reference docs: public-content-provider-README.md (the app-surface contract), public-content-solution-README.md (the cdn-pub solution: registry tiers, serving, split-origin CDN deployment), and the step-by-step publish-discoverable-content-README.md recipe.
Main View Configuration
To define a custom main view, declare ui.main_view in app configuration and mark a method with @ui_main. Code can supply defaults; the app descriptor can override them.
def configuration_defaults(self):
return {
"ui": {
"main_view": {
"src_folder": "ui/main",
"build_command": "npm install && OUTDIR=<VI_BUILD_DEST_ABSOLUTE_PATH> npm run build",
}
}
}
The built SPA communicates with the backend through the app operations endpoint and receives runtime config from GET /api/cp-frontend-config with parent-frame postMessage as fallback. The UI is a normal platform client: if it needs app-originated events targeting one exact connected peer, it must propagate the connected peer id on REST requests per the client communication contract. Embedded pages should emit the same kdcube-resize height/width message used by widgets.
Memory Widget and Reconciliation
Apps that inherit BaseEntrypointWithMemory or BaseEntrypointWithEconomicsAndMemory get the SDK memory widget operations: list/create/update/delete, snapshots, snapshot export/restore preview, and reconciliation jobs. Enable the widget through app props under memory.enabled and memory.widget.enabled, and configure the built widget source under ui.widgets.memories.
Manual reconciliation is submitted through memories_widget_reconcile_run. The request may include agent_type with one of lite, regular, or strong. At execution time that selector maps the logical memory.reconciler role to memory.reconciler.lite, memory.reconciler.regular, or memory.reconciler.strong for that job only. Configure those role models in the app's role_models props.
config:
role_models:
memory.reconciler.lite:
provider: anthropic
model: claude-haiku-4-5-20251001
memory.reconciler.regular:
provider: anthropic
model: claude-sonnet-4-6
memory.reconciler.strong:
provider: anthropic
model: claude-opus-4-1
If the app needs extra per-job controls, send them in reconciliation_context and override on_memory_reconciliation_request(request=...) to validate or augment them before the job is stored. The SDK persists this context with the job and rebinds it under bundle_call_context.memory.reconciliation.context when the background reconciler runs.
async def on_memory_reconciliation_request(self, *, request: dict) -> dict | None:
context = dict(request.get("reconciliation_context") or {})
context.setdefault("policy", "strict")
return {
"agent_type": request.get("agent_type") or "regular",
"reconciliation_context": context,
}
Registry vs. Interface Discovery
GET /api/admin/integrations/bundles returns configured app registry entries: id, repo, ref, module, path, version, and related deployment metadata. The richer interface discovery surface lives on the per-app integrations endpoint above, where the runtime scans decorators and returns the current manifest.
Common App Operations
| Operation | Description |
|---|---|
ai_bundles | App admin dashboard (technical operation name retained) — available through the platform base entrypoint |
control_plane | Control-plane dashboard — via the platform base entrypoint |
economic_usage / opex | Economics and operational usage views — via BaseEntrypointWithEconomics |
memories and memories_widget_* | Memory UI plus memory operations — via BaseEntrypointWithMemory |
suggestions | Suggested prompts for new conversations |
inventory-reorder-api | Example structured API for an inventory widget: list low-stock items, create reorder requests, schedule replenishment scans, trigger immediate execution |
Deploying Your App
Option A: With the KDCube Platform
-
1
Push your app to Git
git push origin v1.0.0 -
2
Add to bundles.yaml
- id: "my-bundle@1-0" repo: "git@github.com:org/my-bundle.git" ref: "v1.0.0" module: "my_bundle.entrypoint" -
3
Initialize secrets and apply
kdcube init --tenant <tenant> --project <project> \ --descriptors-location /path/to/descriptors \ --set-secret services.openai.api_key "sk-..." \ --set-secret services.anthropic.api_key "sk-ant-..." \ --set-secret services.brave.api_key "..." \ --set-secret services.git.http_token "github_pat_..." \ --set-secret git.http_token "github_pat_..." kdcube info --tenant <tenant> --project <project>The CLI writes these values into the staged active
config/secrets.yaml, resolves descriptor placeholders such as tenant/project/domain where the descriptor set supports it, and then the runtime reads that staged copy. -
4
Set the default app in the Admin Dashboard
Open the app dashboard at the technically named
ai_bundlesoperation. Your registered app appears in the list. Setdefault_bundle_idfor the tenant/project. The change is applied immediately; no restart is needed.
Option B: Standalone (Without Platform)
App logic can also run outside the platform. The SDK is a Python package, so you can import selected building blocks from your own FastAPI service or custom image. KDCube adds the integrated hosting, identity, streaming, storage, economics, and UI runtime around that logic; adopting those capabilities remains incremental.
App Source Git Authentication
| Mode | bundles.yaml | Secret |
|---|---|---|
| SSH key | git@github.com:org/repo.git | SSH key mounted in container |
| HTTPS token | https://github.com/org/repo.git | services.git.http_token / git.http_token in secrets.yaml |
Fast Local App Development
KDCube supports a descriptor-driven local app loop optimized for fast changes. Initialize once from descriptors, edit the app under the mounted app source root, then reload only that app without reinstalling the platform. The CLI retains bundle in these command names for compatibility.
# one-time initialization
kdcube init --tenant <tenant> --project <project> \
--descriptors-location /path/to/descriptors \
--set-secret services.openai.api_key "sk-..." \
--set-secret services.anthropic.api_key "sk-ant-..."
kdcube start --tenant <tenant> --project <project>
# then during development
# edit files under assembly.paths.host_bundles_path
kdcube bundle reload my.bundle@1.0.0 --tenant <tenant> --project <project>
# rebuild platform images later (descriptors preserved)
kdcube refresh --tenant <tenant> --project <project> --build
kdcube refresh --tenant <tenant> --project <project> --path /path/to/kdcube-ai-app --build
kdcube refresh --tenant <tenant> --project <project> --release <ref> --build
# reapply seed bundle descriptors only
kdcube bundle config apply --tenant <tenant> --project <project> \
--descriptors-location /path/to/descriptors --dry-run
kdcube bundle config apply --tenant <tenant> --project <project> \
--descriptors-location /path/to/descriptors --reload
The important path rule is that the host app lives under paths.host_bundles_path in assembly.yaml, while bundles.yaml points to its container-visible path:
# assembly.yaml
paths:
host_bundles_path: "/Users/you/dev/bundles"
# bundles.yaml
bundles:
items:
- id: "my.bundle@1.0.0"
path: "/bundles/my.bundle"
module: "entrypoint"
Use this flow for local code changes, descriptor-backed app config edits, and quick widget/API iteration. init stages the active descriptor set under the concrete runtime workdir. bundle reload replays that staged descriptor, clears the app cache, and makes the next request load updated code. bundle config apply stages only bundles.yaml and optional bundles.secrets.yaml; it does not rebuild images or restart Docker. Before replacing live descriptors with older seed files, use kdcube export --out-dir ... to write reviewable snapshots.
Expose a CLI-Started Local Runtime with Ngrok
For Telegram webhooks, Telegram Mini Apps, Cognito callbacks, or any other external callback into a local KDCube runtime, run KDCube with the CLI first and expose the CLI web proxy port with ngrok. Do not expose proc separately.
kdcube start --tenant <tenant> --project <project>
# use the port printed by kdcube start, commonly 5173 for local descriptors
ngrok http --host-header=rewrite 5173
The CLI-started Docker Compose stack already includes the KDCube web proxy, which routes /api/integrations/* to proc, /api/* and /sse/* to ingress, and browser routes to the frontend. After ngrok gives you https://<ngrok-domain>, use that single origin in CORS/Cognito callback settings and app integration URLs such as Telegram webhooks. If assembly.yaml changes, restart with kdcube refresh --tenant T --project P; use --path, --latest, --upstream, or --release <ref> only when platform source should change. If app config or secrets change, use kdcube bundle reload <bundle_id> or reapply seed descriptors with kdcube bundle config apply --descriptors-location ... --reload.
Reference recipe: ngrok-README.md.
App-Scoped @venv(...) Helpers
Apps can mark dependency-heavy helper functions with @venv(...). The platform creates a cached per-app subprocess venv, overlays the runtime packages already present in proc, then installs the app's requirements.txt on top. The venv is rebuilt only when the requirements file changes.
from kdcube_ai_app.infra.plugin.bundle_loader import api, venv
@venv(requirements="requirements.txt", timeout_seconds=120)
def parse_external_document(payload: dict) -> dict:
...
class MyBundle(BaseEntrypoint):
@api(alias="ingest_document", route="operations")
async def ingest_document(self, **kwargs):
result = parse_external_document(kwargs)
await self.comm.event(
type="bundle.ingest.completed",
step="ingest_document",
status="completed",
title="Document processed",
data={"pages": result.get("pages", 0)},
)
return result
Boundary rule: @venv(...) is for plain serialized inputs and outputs only. Keep communicator use, request context, DB pools, Redis clients, and other live proc-bound runtime objects outside the venv helper. If only requirements.txt changes, the next helper call rebuilds the cached venv lazily. If Python source changes, use the normal app reload path.
App-Scheduled @cron(...) Jobs
Apps can declare proc-owned scheduled jobs directly in app code with @cron(...). This binds periodic app logic to the platform lifecycle without requiring a separate external scheduler for every routine.
from kdcube_ai_app.infra.plugin.bundle_loader import cron
class MyBundle(BaseEntrypoint):
@cron(
alias="rebuild_index",
expr_config="routines.rebuild.cron",
cron_expression="0 */6 * * *",
span="instance",
)
async def rebuild_index(self):
...
The shipped contract is: @cron(alias=..., cron_expression=..., expr_config=..., span=...). Use cron_expression for an inline schedule or expr_config for a dot-separated config path such as apps.app1.routines.cron. If both are provided, expr_config wins. If the resolved config value is missing, blank, or disable, the job is inert and nothing is scheduled. span controls exclusivity across process, instance, or system.
A practical example is a scheduled inventory routine that materializes the next reorder queue and publishes an operational event for the UI:
class InventoryBundle(BaseEntrypoint):
@cron(alias="refresh-reorder-queue", cron_expression="0 6 * * *", span="system")
async def refresh_reorder_queue(self):
items = await self.rebuild_reorder_queue()
await self.comm.event(
type="bundle.inventory.reorder_queue_refreshed",
step="refresh_reorder_queue",
status="completed",
title="Reorder queue refreshed",
data={"items": len(items)},
)
Background Job Stream and @on_job
For work that should not run inside a scheduler tick or widget request, enqueue a background job and handle it with @on_job. The producer creates the durable app-owned record first, then writes a ready-work envelope to Redis Streams. Proc claims jobs fairly across workers, builds the app runtime context, and invokes the app's async @on_job handler.
from kdcube_ai_app.infra.plugin.bundle_loader import cron, on_job
class TaskBundle(BaseEntrypoint):
@cron(alias="due-scan", cron_expression="*/5 * * * *", span="system")
async def due_scan(self):
await self.tasks.enqueue_due_jobs()
@on_job
async def on_job(self, job: dict, **kwargs):
del kwargs
if job.get("work_kind") == "task.execution.due":
return await self.tasks.run_execution(job["payload"]["execution_id"])
return {"ok": False, "error": {"code": "unsupported_job"}}
@on_job is not a public route and not a widget operation. It should validate work_kind, load durable ids from payload, and update app-owned execution or result state. Until proc acknowledges the stream message, retry is possible.
Reference Apps
| Technical app ID | Use it for | Key surfaces |
|---|---|---|
workspace@2026-03-31-13-36 | Ready ReAct workspace and full app composition | Chat, scene, canvas, tools, skills, MCP consumers, Telegram, main view, widgets, files, and economics |
ported-langgraph-agents@2026-07-13 | Bring an existing LangGraph/LangChain agent | Two agents behind one app, per-turn graph rebuild, conversation streaming, capabilities, isolated code execution, and shared-schema persistence |
kdcube-services@1-0 | Service-oriented app package | Named-service providers, MCP facade, Data Bus relay, public file hosting routes, OpenAPI, storage ownership, and contract tests |
connection-hub@1-0 | Identity and delegated connections | Connected accounts, OAuth/OIDC, delegated automation, consent, widgets, APIs, grants, and user-secret resolution |
website@2026-07-12 | Application-hosted website | Main-view site, alias/host declaration, multipage files, SPA fallback, injected site context, and CDN origin routing |
workspace — widgets, Telegram, operations, and custom main-view pattern
The workspace sample shows how one app can combine a ReAct conversation workflow with decorated widget endpoints, APIs, Telegram webhook/Mini App surfaces, attachment handling, and a custom SPA main view. It is the best current public reference for how entrypoint methods, per-agent consumer configuration, ui/main/, and ui/widgets/ fit together.
Documentation Reference
SDK & App Authoring
- bundle-index-README.md
- bundle-developer-guide-README.md
- bundle-agent-integration-README.md
- workspace-reference-bundle-README.md
- bundle-client-ui-README.md
- chat-component-communication-README.md
- chat-stream-events-README.md
- bundle-events-README.md
- bundle-event-recording-and-sinks-README.md
- bundle-platform-integration-README.md
- bundle-runtime-configuration-and-secrets-README.md
- bundle-delivery-and-update-README.md
- bundle-venv-README.md
- tool-subsystem-README.md
- custom-tools-README.md
- mcp-README.md
SDK Integrations
Deployment Descriptors
Agent Harness [full folder →]
- harness/README.md
- artifact-resolution-and-materialization-README.md
- turn-log-README.md
- turn-view-README.md
- provider-projection-README.md
- conversation-artifacts-README.md
- references-and-paths-README.md
- workspace-model-README.md
- artifact-storage-README.md
- workspace-lifecycle-and-distribution-README.md
ReAct Agent [full folder →]
- flow-README.md
- react-context-README.md
- timeline-README.md
- git-backed-workspace-engineering-README.md
- artifact-discovery-README.md
- how-to-construct-react-agent-README.md
- round-generation-feedback-README.md
- source-pool-README.md
- external-exec-README.md
- event-blocks-README.md
- tool-call-blocks-README.md
- event-subsystem-README.md
- external-events-README.md
- event-source-README.md
- block-production-README.md
Current Capability Index
This table summarizes implemented app surfaces. The owning repository docs remain authoritative for exact contracts and configuration.
| Feature | Status | Notes |
|---|---|---|
| App-declared widgets | Available | Apps mark their UI widgets explicitly with @ui_widget. The platform exposes that widget manifest via GET /api/integrations/bundles/{tenant}/{project}/{bundle_id} and /widgets, so frontends can render app-defined launchers from discovered metadata instead of hardcoded platform lists. |
| Custom main view | Available | Apps can provide a custom main UI through ui.main_view config and a discovered @ui_main entrypoint. A main view can be a full SPA using REST, SSE streaming, widgets, downloads, and any richer client behavior the app needs. |
| Static asset serving from an app | Available | App-scoped static files can be served directly via GET /api/integrations/static/{tenant}/{project}/{bundle_id}/{path}. GET operations are also available for idempotent app APIs under /operations/{operation}. |
| Public app APIs | Available | App APIs can be exposed under /public/{operation} through @api(..., route="public"). Public methods declare public_auth, including intentionally open access, proc-side header-secret verification, and app-owned verification. |
| App-served MCP endpoints | Available | Apps can expose MCP-native HTTP endpoints through @mcp(...). Proc routes them under /mcp/{alias} or /public/mcp/{alias}. Managed mode applies delegated-credential checks; otherwise the app owns MCP authentication and authorization. |
| App surface access | Available | Where proc owns the route, app listing and per-method visibility use declared allowed_roles, user_types, and raw roles. user_types use threshold semantics. Public APIs additionally declare public_auth. MCP uses either managed delegated credentials or app-owned authentication, not proc-side user_types. |
| Admin app interface discovery | Available | /api/admin/integrations/bundles returns registry metadata enriched with scanned app manifest data such as widgets, apis, on_message, on_job, and scheduled jobs. Admins can see what an app exposes without opening its code. |
| Fast local app development | Available | Point assembly.yaml at paths.host_bundles_path, define the app in bundles.yaml with a container path under /bundles, initialize once, then iterate with the compatibility command kdcube bundle reload <bundle_id>. The live local descriptor authority is the staged runtime copy under workdir/config/. See how-to-configure-and-run-bundle-README.md. |
| Live app reload | Available | App code is loaded per process and may be cached as a singleton, but local development does not require restarting proc. kdcube bundle reload <bundle_id> replays the staged descriptor and clears the app cache for the next request. Admin updates persist into the live descriptor or secret authority before reload applies them. |
on_props_changed(...) lifecycle hook |
Available | BaseEntrypoint exposes on_props_changed(...) for reconciling long-lived side effects after effective app props change. Already-loaded singleton apps in the current worker receive it on live bundles.props.update events. |
on_apply_props(props) configuration hook |
Available | BaseEntrypoint calls this public async hook after platform-reserved props on every turn, REST, widget, MCP, and local-operation apply path. Return True only when the model service must be rebuilt; do not override the internal _apply_bundle_props_overrides. |
| App versioning | Available | Update the ref (branch, tag, or commit) through the configured descriptor write path: file-backed bundles.yaml, App Admin, or CLI apply. Redis and provider-generated runtime YAML projections are derived views, not alternate write authorities. See bundle-runtime-configuration-and-secrets-README.md. |
| Deployment-scoped app props and secrets | Available | Apps read deployment-scoped non-secret config with self.bundle_prop(...) and app secrets with await get_secret("b:..."). Non-secret props belong in the configured descriptor authority; secrets belong in the configured secrets provider. In local file mode those are bundles.yaml and bundles.secrets.yaml. See bundle-runtime-configuration-and-secrets-README.md. |
| User Settings | Available | Async typed stores over user_bundle_props hold durable user choices; user secrets remain in the configured user-scoped secrets provider. Agent model, instruction profile, tool, skill, MCP, named-service, and subagent selections are saved per conversation under conversation:<conversation_id>:agent_selection:<agent_id>. An optional baseline seeds a new conversation once, and picker drafts persist only on Save changes. |
| Connection Hub and per-agent delegation | Available | Connected provider accounts (Delegated to KDCube) remain separate from hosted-agent, external-client, and automation grants (Delegated by KDCube). Hosted agents use kdcube-agent:<app>:<agent>; concrete operations re-check provider capability and caller/account binding and raise scoped consent demands at attempt time. |
| Governed generated-code execution | Available | The reference Docker split profile runs generated code in a separate networkless, read-only-root executor with narrow workspace mounts. Tool calls cross to a trusted supervisor carrying identity and credentials. Local subprocess is development containment, combined Docker is a legacy shared-container profile, and Fargate uses a different one-task physical boundary. |
| Economics and accounted service use | Available | Deployment and live Eco Admin policy cover plans, quotas, budgets, reservations, prices, trials, wallet credit, and participating model, embedding, web-search, and custom service calls. Coverage applies only to integrated accountable calls, not arbitrary unintegrated spending. |
| User-reconcilable durable memory | Available | Apps can expose durable user memories with evidence, snapshots, create/update/delete operations, export/restore preview, and explicit background reconciliation. Conversation history, durable user memory (mem), and the conversation named-service realm (cnv) remain distinct. |
| Runtime feature gating and configurable visibility | Available | Apps can gate the whole app through the technically named enabled.bundle prop and configure API/widget visibility with allowed_roles_config, user_types_config, and roles_config. Do not use removed resource-level enabled_config decorator arguments. See bundle-platform-integration-README.md. |
App-scoped @venv helpers |
Available | Apps can run selected dependency-heavy helper functions in a cached per-app subprocess venv. The venv inherits platform runtime packages, installs the app's requirements.txt, and rebuilds only when the requirements hash changes. It is for leaf work, not live proc-bound runtime objects. |
| App-local Node / TS backend sidecar | Available | Apps can keep the public KDCube surface in Python and run selected backend logic as an app-local Node or TypeScript sidecar. This wraps existing Node backends without discarding them. Startup-config drift triggers lazy restart on next use. |
App-scheduled @cron jobs |
Available | Apps declare scheduled jobs with @cron(alias=..., cron_expression=..., expr_config=..., span=...). The scheduler resolves configured or inline schedules and supports process, instance, and system exclusivity spans. |
App background @on_job handlers |
Available | Apps can declare one async @on_job handler for ready work claimed by proc from the Redis background job stream. @cron or an operation can enqueue; @on_job validates work_kind, reads durable ids, and updates app-owned result state. |
| Git-backed React workspace | Available | ReAct supports a sparse git-backed workspace with lineage-isolated history, explicit materialization through react.pull(...), and workspace construction through react.checkout(mode="replace"|"overlay", ...). Editable state lives under turn_<id>/git/projects/..., deliverables under turn_<id>/files/..., and snapshots under turn_<id>/git/snapshots/.... Conversation-owned refs use qualified conv:fi: form. |
| App event sources and artifact rehosters | Available | External/provider events declare owner discovery, pull, and phase-bound block-production policies; they are not model-callable tools or event transport. Tool results can use related block-production contracts. Registered namespace rehosters let a framework adapter materialize authorized refs such as ext:... into qualified conv:fi: refs. |
| Per-agent ReAct runtime configuration | Available | Apps configure each ReAct agent under config.react.<agent>, including instructions, ID-based instruction profiles, supported models, role models, and iteration bounds. Tool, skill, MCP, named-service, and subagent inventory lives under surfaces.as_consumer.agents.<id>; users can narrow that administrator ceiling per conversation but cannot widen it. |
| Claude Code agent integration | Available | Apps can integrate the Claude Code agent as an accountable LLM runtime, including per-turn model, token, cache, and spend reporting plus optional git-backed session continuity. |
| Interactive steer / followup execution | Available | Busy-turn followup and steer enter the shared conversation event lane. The live ReAct runtime folds them into the owned turn; authored app events use the same lane identity with explicit agent_id, event_source_id, and routing.reactive. |
| Streaming for operations | Available | App operations can stream to connected clients. REST calls can target the initiating peer with KDC-Stream-ID, and app code can emit deltas, steps, and widget updates through the communicator over SSE or Socket.IO while the operation is running. |
| Tool traits | Available | strategy traits govern ordered multi-action compatibility; execution traits govern completed-call scheduling and replay. The supported early profile is fully validated, neutral, detached, parallel_with_generation, and at_most_once_per_round. |
| ReAct subagents | Available | When administrator config and user selection both allow it, react.delegate launches a chartered helper as its own fair-scheduled child conversation. Denial prevents spawner installation, the tool, and delegation guidance. Contributions and terminal events return to the parent timeline; this is distinct from fenced runtime execution. |
| Application-hosted websites | Available | An app can expose its built main view as a complete site. A validated ApplicationSiteCatalog is distributed through Redis generations and served from a proc-local immutable snapshot with alias, host-first/default routing, multipage files, SPA fallback, and CDN origin support. |