KDCube is built around apps
Everything you ship on KDCube lives inside an app. The app is the single unit of deployment, versioning, tenancy, and lifecycle — every other part of the platform exists to run apps, isolate them, meter them, stream their output, expose their object namespaces, and mount their UI surfaces. When you install KDCube, you are installing the runtime that hosts apps; when you build "an agent," you are authoring an app. The SDK includes both the built-in ReAct subsystem and a separate ISO runtime subsystem that apps can use for controlled execution.
That single decision shapes most of the SDK's ergonomics:
- One authoring surface for the whole application. An app carries Python backend code, optional TypeScript UI widgets and views, tools, event sources, skills, outbound MCP tool integrations, optional app-served MCP endpoints, named-service providers, prompts, configuration, hosted file outputs, and storage layout — all in one package, versioned together.
- Hot-loadable without restarting the runtime. Apps can be replaced or upgraded in place; the ingress and processor services stay up. This is what makes "deploy" a small operation, not a downtime event.
- Tenant-scoped by construction. An app runs against a
(tenant, project, user)context the platform resolves at admission. Storage paths, budgets, decision logs, and streaming channels inherit that scope — the app doesn't have to enforce it manually. - Metered and billed as a unit. Every model call, tool invocation, and storage write routed through the app is tagged for economics accounting, so per-tenant spend, audit, and rate limits apply without extra plumbing.
- Framework-agnostic inside. An app can use the built-in ReAct 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.
Environment rule: one tenant/project is one isolated deployment environment. Use a different tenant/project when you need customer isolation or separate lifecycle stages such as dev, staging, and prod. Keep multiple apps inside one tenant/project when they belong to the same environment.
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.
Most example apps use the ReAct agent. The current runtime features ReAct v3, selected through AI_REACT_AGENT_VERSION=v3. The ReAct subsystem gives apps tools, skills, continuous conversations, a sources pool, ANNOUNCE, a signals board, named-service tools, tool lifecycle events, and an event timeline for tool calls, authored external events, followup, and steer. AI_REACT_AGENT_MULTI_ACTION=safe_fanout enables limited multi-action rounds in v3, but those actions still execute sequentially, not in parallel. The ReAct round cap can be set globally with ai.react.max_iterations / AI_REACT_MAX_ITERATIONS and overridden per app with config.react.max_iterations or react.max_iterations in app props. An app does not have to use ReAct at all: you can use any LangGraph graph, the ISO runtime as a separate subsystem, a simple request→response pipeline, 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 ordinary fi: paths. |
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 is declared in a loaded tool module or event module and must choose a destination that matches ReAct workspace semantics: snapshots go to fi:turn_<id>.snapshots/..., editable workspace files go to fi:turn_<id>.files/..., produced artifacts go to fi:turn_<id>.outputs/..., and domain attachments go to fi:turn_<id>.external.<event_kind>.attachments/<event_id>/.... After react.pull(paths=["ext:..."]), agents continue with the returned logical_path or physical_path rows.
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 versatile@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 |
|---|---|
versatile@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 a multi-tenant AI application platform. It provides the infrastructure for deploying conversational AI agents at scale — with streaming, storage, economics, tooling, and extensibility all built in.
An app is your unit of deployment. It is a Python package, usually with optional UI sources, that plugs into the platform and defines how your AI agent thinks, what tools it uses, how it stores data, which object namespaces it exposes, what UI widgets it mounts, and how it bills users.
Big Picture: How It All Fits Together
Event Bus and Data Bus
Apps communicate with user-facing surfaces through two platform planes. The event bus carries discrete state changes: tool calls, tool results, protocol errors, external events, canvas updates, app-emitted notifications, and UI commands. The data bus carries heavier or replayable data: hosted files, materialized objects, search result payloads, canvas snapshots, streamed artifacts, and provider-owned object projections.
The processor is the trust boundary for both planes. It binds tenant/project/user context, applies app event-firewall policy before outbound events reach clients or recorders, resolves named-service providers for object actions, and stores durable artifacts through the configured storage backend. Ingress fans out permitted event-bus messages to connected browser clients; widgets and scenes can then render those events or fetch data-bus payloads by stable refs. The fuller bus diagram is in Platform.
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 Python package with a required entrypoint.py and optional supporting files:
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):
@property
def configuration(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 (property) | On app load | Declare defaults: role_models, embedding, react, knowledge, economics, execution |
on_bundle_load(**kwargs) | Once per process, per tenant/project | Build knowledge index, connect external services, warm caches |
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_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 heavy preparation work. Do not store request-local state there.
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 bundle-owned location, then rebind it when that job runs. PORTABLE_SPEC_JSON is platform-built; bundle-owned per-call data belongs in 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 bundle 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 bundle code | Promoted runtime config assembled from env plus descriptor files such as assembly.yaml and gateway.yaml | Ports, auth ids, storage backends, runtime path roots | Outside kdcube export; manage through deployment descriptors |
| Platform/global secrets | async read: await get_secret_async("canonical.key")write: none from normal bundle code sync helper remains for compatibility | 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(...) | Mounted writable bundles.yaml when present; Redis is the runtime cache; grouped descriptor docs are fallback only when no mounted file exists | App feature flags, cron config, model selection, UI config | Exported to bundles.yaml by kdcube export |
| App secrets | async read: await get_secret_async("b:...")write: await set_bundle_secret(...)sync helper remains for compatibility | 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 | read: get_user_prop(...), get_user_props()write: set_user_prop(...), 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 | async read/write: await get_user_secret_async(...), await set_user_secret_async(...), await delete_user_secret_async(...)sync helpers remain for compatibility | Configured secrets provider; in local secrets-file mode this is secrets.yaml | 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 |
| App storage backend | app storage APIs | S3 or local/file-backed storage, depending on deployment | Persistent business data and large generated outputs | 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.
For isolated execution, Docker/Fargate supervisors receive descriptor payloads and materialize them before tool bootstrap. App tools therefore read the same platform props, platform secrets, app props, and app secrets through the normal SDK helpers, while generated code does not receive descriptor files or provider secret env. Set execution.runtime.descriptor_payload_scope: active_bundle when an app runtime should receive only its own bundles.yaml and bundles.secrets.yaml sections.
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_async("b:...") resolves the current app from the bound runtime context. During normal app execution that context is already present; outside a bound request or app runtime, use a fully qualified secret path instead of b:. Use the async helpers in new app APIs, tools, cron handlers, and @on_job handlers; sync secret helpers are compatibility APIs for old sync-only code.
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
📥 Inbound: ChatTaskPayload
Every 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
📤 Outbound: Chat Streaming
Your bundle streams back via 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
Bundles 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(...)
Platform child tool runtimes, including TOOL_RUNTIME[tool_id] = "local", 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 bundle exposes additional REST operations via 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 bundle's decorator-discovered interface surface rather than assuming every operation is just workflow.run(). A default-bundle 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 is a distributed, multi-tenant system. Three storage tiers are available to your bundle:
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/")
Local FS (Shared Bundle 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}/. Use this helper-resolved storage for local bundle state that should survive across requests on the same instance. Typical examples are knowledge indexes, cloned repos, cron workspaces, prepared local caches, or mutable subsystem roots such as _task_tracker or _knowledge_base_admin. Do not keep operational state next to the bundle source tree. In production, mount the shared instance-visible storage layer here; today that is commonly EFS.
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
The platform uses LangGraph for workflow orchestration. Your execute_core builds and invokes a StateGraph. For common patterns, use BaseWorkflow:
from kdcube_ai_app.apps.chat.sdk.solutions.chatbot.base_workflow import BaseWorkflow
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:
react = await self.build_react(
scratchpad=scratchpad,
tools_module="my_bundle.tools_descriptor",
skills_module="my_bundle.skills_descriptor",
additional_instructions=self.bundle_prop("react.additional_instructions"),
)
result = await react.run(payload)
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.
react.additional_instructions is now a first-class bundle prop. If present, pass it through build_react(...) so the bundle can append deploy-scoped prompt guidance to the React decision system prompt without copying or forking the base runtime prompt.
build_react() respects runtime selection through AI_REACT_AGENT_VERSION. The featured runtime is v3. If AI_REACT_AGENT_MULTI_ACTION=safe_fanout is enabled, accepted multi-action rounds are still executed sequentially. The base round cap resolves from bundle props config.react.max_iterations / react.max_iterations, then assembly/env ai.react.max_iterations / AI_REACT_MAX_ITERATIONS, 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 | Context retrieval and RAG |
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 | Isolated Python code execution — Docker (default), Fargate, or in-process per bundle config |
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 artifacts by logical path (ar:, fi:, ks: addresses) |
read | react → react.read | Load artifact into timeline (fi:/ar:/ks:/so: paths) |
write | react → react.write | Author current-turn content into files/<scope>/... for durable workspace state or outputs/<scope>/... for non-workspace artifacts; stream to canvas/timeline_text/internal channel and optionally share as file. Internal writes are files by default; scratchpad=true adds a short inline react.note. |
pull | react → react.pull | Explicitly materialize same-turn, historical, cross-conversation, or registered custom namespace refs as local reference material. fi: refs map by convention; custom refs such as ext:... are resolved by a namespace rehoster, and the returned rows tell the agent the materialized fi: path. Pulled refs do not modify the active current-turn workspace. |
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 the active current-turn workspace from ordered fi:...files... refs. mode="replace" seeds the workspace from scratch; mode="overlay" imports selected historical files into the existing workspace. It does not accept ext:, outputs, snapshots, or attachments directly. |
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 registered in tools_descriptor.py. 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.
In ReAct, a tool call is also a special case of an event source: the tool invocation produces timeline blocks, may contribute source rows or hosted artifacts, and can participate in the same event-source policy plane as external UI/service events. Most custom tools can rely on the default tool-result block production policies; only tools that need custom timeline rendering, custom artifact rehosting, or custom phase behavior need extra event-source decorators.
# 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..."
# tools_descriptor.py
TOOLS_SPECS = [
# SDK built-in tool modules (installed package)
{"module": "kdcube_ai_app.apps.chat.sdk.tools.web_tools", "alias": "web_tools", "use_sk": True},
{"module": "kdcube_ai_app.apps.chat.sdk.tools.exec_tools", "alias": "exec_tools", "use_sk": True},
# App-local tools ("ref" = path relative to app root, works in Docker too)
{"ref": "tools/my_tools.py", "alias": "my_tools", "use_sk": True},
]
# Tool IDs: web_tools.web_search, exec_tools.execute_code_python, my_tools.search
# Optional: per-tool runtime overrides
TOOL_RUNTIME = {
"web_tools.web_search": "local", # subprocess sandbox
"exec_tools.execute_code_python": "docker", # Docker container
}
File-producing tools have two supported paths. They can return local files declaratively:
{
"ok": true,
"error": null,
"ret": {
"artifact_type": "files",
"files": [
{
"path": "outputs/report.pdf",
"filename": "report.pdf",
"mime": "application/pdf",
"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. Both paths create normal hosted file metadata and emit chat.files to connected clients. 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 the normal materialized fi: path for later react.read, react.rg, or generated-code use.
See custom-tools-README.md and the reference bundle’s versatile tools_descriptor.py.
MCP (Model Context Protocol) servers are declared in MCP_TOOL_SPECS and configured in the app consumer surface.
# tools_descriptor.py
MCP_TOOL_SPECS = [
{"server_id": "web_search", "alias": "web_search", "tools": ["web_search"]},
{"server_id": "docs", "alias": "docs", "tools": ["*"]}, # all tools
{"server_id": "stack", "alias": "stack", "tools": ["*"]},
]
# Tool IDs: mcp.docs.some_tool, mcp.stack.some_tool
# bundles.yaml config section — how to connect
config:
surfaces:
as_consumer:
mcp:
services:
mcpServers:
docs:
transport: http
url: https://mcp.example.com
auth:
type: bearer
secret: bundles.my-bundle.secrets.docs.token
stack:
transport: stdio
command: npx
args: ["mcp-remote", "mcp.stackoverflow.com"]
See mcp-README.md for all transports (stdio, http, streamable-http, sse) and auth modes.
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 metadata
provider.about, object.schema, search scopes, search filters, object capabilities, and default open-effect action tell the agent and UI how to handle the namespace.
Consumer policy
The tool connection declares allowed operations per namespace and strategy traits such as exploration, neutral, and exploitation.
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
Artifact refs describe where data came from; they do not all mean "current editable project file." ReAct's current workspace is turn_<current>/files/.... Reports and generated deliverables belong in outputs/..., workflow state belongs in snapshots/..., and uploads or domain attachments belong in attachments/... or external/.... Use react.pull to materialize refs before local search/read by filesystem path; use react.checkout only when historical files/ refs must become editable current-turn workspace state.
| Prefix | Resolves To | Example |
|---|---|---|
fi:...files... | Durable workspace/project state for a turn; historical files/ refs can be checked out into the current workspace | fi:turn_123.files/app/src/main.py |
fi:...outputs... | Produced artifacts, reports, render sources, screenshots, PDFs, diagnostics; not editable workspace state by default | fi:turn_123.outputs/export/report.pdf |
fi:...snapshots... | Story, wizard, canvas, or workflow state snapshots | fi:turn_123.snapshots/wizard/current.yaml |
fi:...attachments... | User uploads and hosted attachment files for a turn | fi:turn_123.user.attachments/screenshot.png |
fi:conv_... | Cross-conversation artifact ref; materializes under conv_<conversation_id>/turn_... | fi:conv_support-42.turn_123.snapshots/wizard/current.yaml |
ext: or another custom namespace | Opaque bundle/domain artifact ref. Valid only when a loaded module registers a namespace rehoster; react.pull returns the materialized fi: path. | ext:inventory/reorder_42/stock-snapshot.yaml |
ar: | Artifact from timeline | ar:turn_123.artifacts.summary |
ks: | Knowledge space (read-only, docs/src) | ks:docs/architecture.md |
sk: | Skills space (skill instruction files) | sk:public.pdf-press/SKILL.md |
so: | Sources from context pool | so:sources_pool[1-5] |
tc: | Tool call block | tc:turn_123.abc.call |
Reusable SDK Integrations
SDK integrations are product-neutral building blocks that bundles import when they need external protocol support. They keep provider mechanics and transport details in the SDK while the bundle keeps user policy, routing, and workflow decisions.
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.
Integration Boundary
Bundles own product policy; SDK integrations own reusable mechanics. Use thin bundle 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 | Generate hosted URLs for file artifacts |
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 |
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 (referenceable asso:sources_pool[...])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 |
@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_message | method | Marks the message-entry method the proc/web layer can route to for live message handling | no args |
@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 |
@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 |
Bundle identity can come from the registry entry or be declared explicitly with @bundle_id("my.bundle@version"). The loader uses the discovered bundle interface decorators as the HTTP, UI, message, and job contract. Use user_types=(...) for inferred platform user types such as anonymous, registered, paid, or privileged, and use roles=(...) for raw external auth roles such as kdcube:role:super-admin. user_types are threshold-based, not exact-match: the order is anonymous < registered < paid < privileged, so user_types=("registered",) means registered-or-higher, and user_types=("paid",) means paid-or-higher. If both user_types and roles are provided, both checks must pass. Add user_types_config and roles_config to APIs/widgets when Bundle Admin should override those defaults from bundle props. Add allowed_roles_config to @bundle_entrypoint when bundle-level listing visibility should be configurable. Resource enabled state is no longer a decorator argument for APIs or MCP; use bundle props/Admin resource overrides. That threshold rule applies to @api and @ui_widget when proc owns the route auth contract. @api(route="public") now has three explicit auth shapes: public_auth="none", built-in proc-side header_secret, and bundle-owned public_auth="bundle". @mcp is different again: proc does not enforce user_types, roles, or public_auth there; the bundle MCP app owns request authentication and authorization. @on_job is also different: it is not URL-addressable, must be async, and receives jobs already admitted to the background job stream. If an entrypoint inherits SDK mixins, the decorated @on_job method should call await super().handle_job(**kwargs) before handling custom work_kind values.
Example: Declare widget, API, MCP, and UI entrypoints
from fastapi import HTTPException, Request
from kdcube_ai_app.apps.chat.sdk.config import get_secret_async
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"))
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_async("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-Bundle-MCP-Token")
expected_token = await get_secret_async("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)
@api(..., route="public", public_auth="bundle") when the bundle, not proc, must verify the inbound request. Put the client-facing header name in bundles.yaml -> items[].config.telegram.webhook.auth.header_name, put the verification token in bundles.secrets.yaml -> items[].secrets.telegram.webhook.auth.shared_token, accept request: Request in the method, and validate request.headers[...] there.bundles.yaml -> items[].config.mcp.inbound.auth.header_name, put the verification token in bundles.secrets.yaml -> items[].secrets.mcp.inbound.auth.shared_token, then validate request.headers[...] in the MCP provider before returning the FastMCP app. Proc does not verify this token for MCP.Integrations HTTP Surface
| Endpoint | Purpose | Notes |
|---|---|---|
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id} | Return the bundle 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 a bundle API operation | Resolved via @api; preferred explicit form |
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 bundle API operation | Resolved only via @api(route="public", ...); current public methods must declare public_auth; public_auth="bundle" forwards the request for bundle-owned verification |
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/{operation} | GET variant for public idempotent bundle APIs | Supports the same route-scoped manifest contract |
GET|POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/mcp/{alias} | Call a bundle-served MCP endpoint | Resolved only via @mcp(route="operations", ...); current transport is streamable-http; proc forwards headers/body and the bundle authenticates the request itself if it wants auth |
GET|POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/mcp/{alias} | Call a public bundle-served MCP endpoint | Resolved only via @mcp(route="public", ...); this selects the public URL family, but proc still does not apply public_auth for MCP |
POST /api/integrations/bundles/{tenant}/{project}/operations/{operation} | Default-bundle shortcut | Compatibility path when bundle_id is omitted |
GET /api/integrations/static/{tenant}/{project}/{bundle_id}/{path} | Serve bundle-scoped static assets | Useful for bundle-shipped frontend assets and media |
route="public" only for intentionally public endpoints, and declare public_auth explicitly. Current modes are "none" for intentionally open routes, built-in proc-side header_secret, and bundle-owned "bundle" when the bundle should authenticate the hook itself.@mcp(...) is bundle-authenticated, not proc-authenticated. Proc forwards the raw HTTP request into the returned FastMCP or ASGI subapp. Header names, cookies, bearer tokens, HMAC signatures, or custom JWT verification are all bundle-defined concerns for MCP. AUTH.ID_TOKEN_HEADER_NAME is not the MCP auth contract.How to configure and call a bundle-authenticated public API hook
Use the same explicit split as MCP: the bundle defines the non-secret client contract in bundle props, stores verification material in bundle 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 bundle props, for example
X-Telegram-Bot-Api-Secret-Token - the shared token provisioned in bundle secrets
Bundle-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}'
How to configure and call a bundle MCP endpoint
Use one explicit contract. The bundle defines the non-secret client contract in bundle props, stores verification material in bundle secrets, and checks the incoming headers itself before returning the FastMCP app.
Server-side configuration
# bundles.yaml
bundles:
version: "1"
items:
- id: "versatile@2026-03-31-13-36"
config:
mcp:
preferences:
auth:
header_name: "X-Versatile-Preferences-MCP-Token"
# bundles.secrets.yaml
bundles:
version: "1"
items:
- id: "versatile@2026-03-31-13-36"
secrets:
mcp:
preferences:
auth:
shared_token: "<rotate-me>"
Bundle code
@mcp(alias="preferences_tools", route="operations", transport="streamable-http")
async def preferences_tools_mcp(self, request: Request, **kwargs):
header_name = self.bundle_prop(
"mcp.preferences.auth.header_name",
"X-Versatile-Preferences-MCP-Token",
)
expected_token = await get_secret_async("b:mcp.preferences.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 bundle props, for example
X-Versatile-Preferences-MCP-Token - the token provisioned in bundle secrets
Authenticated MCP client call
curl -X POST \
"http://localhost:5173/api/integrations/bundles/demo-tenant/demo-project/versatile@2026-03-31-13-36/mcp/preferences_tools" \
-H "X-Versatile-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"}'
@mcp(...), route="operations" and route="public" select the URL family only. They do not choose the auth verifier. If a bundle wants auth, the bundle verifies it. If a bundle wants a truly public MCP endpoint, it simply does not enforce auth in its MCP provider.Interface Manifest Discovery
Decorators on the bundle class are the source of truth for the bundle's HTTP, UI, message, and job contract. At load time the loader scans the entrypoint class for @api, @mcp, @ui_widget, @ui_main, @on_message, and @on_job decorators and builds a typed BundleInterfaceManifest:
# Internal manifest shape (built automatically by the loader)
BundleInterfaceManifest(
bundle_id="versatile@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.
MCP Tools vs Bundle-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 external MCP servers as tools inside your agent | MCP_TOOL_SPECS in tools_descriptor.py plus app props config.surfaces.as_consumer.mcp.services | Adds outbound tool IDs such as mcp.docs.search to the bundle tool catalog | Inside agent/tool execution rounds |
| Expose an MCP-native surface from your bundle | @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 bundle widget 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 bundle operation URLs from baseUrl, defaultTenant, defaultProject, and defaultAppBundleId. Do not hardcode tenant, project, or bundle id from the source folder. 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, bundle 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
Bundles 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 bundle's stable 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.
on_bundle_load() via _ensure_ui_build(). It resolves src_folder, runs build_command, and stores output under <bundle_storage_root>/ui/. A .ui.signature file skips rebuilds when nothing changed. Build-on-first-request is also supported if the UI was not yet built in the current process.
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 your bundle's configuration property and mark a method with @ui_main:
@property
def configuration(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 bundle 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 bundle-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
Bundles 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 bundle props/config 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 bundle'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 bundle 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 the configured bundle registry entries — id, repo, ref, module, path, version, and related deployment metadata. The richer interface discovery surface lives on the per-bundle integrations endpoint above, where the runtime scans decorators and returns the current manifest.
Common Bundle Operations
| Operation | Description |
|---|---|
ai_bundles | Bundle admin dashboard (props editor, status) — all bundles inherit this |
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 Bundle
Option A: With the KDCube Platform
-
1
Push your bundle 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 as default bundle via the Admin Dashboard
Open the AI Bundles Dashboard (
/api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/ai_bundles). Your registered bundle appears in the list. Set it as thedefault_bundle_idfor the tenant/project. The change is applied immediately via Redis — no restart needed.
Option B: Standalone (Without Platform)
Bundles can run outside the platform. The SDK is a plain Python package — build your own FastAPI app that imports and invokes it directly. Or build a custom Docker image that runs just your bundle with its own server. The platform's value is in hosting, auth, SSE, storage, economics, and UI — none of that is required for the core agent logic.
Bundle Git Auth
| 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 Bundle Prototyping
KDCube supports a descriptor-driven local bundle iteration loop optimized for fast changes. The intended loop is: initialize once from descriptors, edit the bundle locally under the mounted bundles root, then reload just that bundle without reinstalling the platform.
# 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 bundle lives under the host bundles root declared in assembly.yaml, while bundles.yaml points to the 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 bundle config edits, and quick widget/API iteration. init stages the active descriptor set under the concrete runtime workdir. bundle reload replays that active staged descriptor, clears bundle caches, and makes the next request load the updated bundle code. bundle config apply stages only bundles.yaml and optional bundles.secrets.yaml; it does not rebuild images or restart Docker. Before replacing live runtime bundle descriptors with older seed files, use kdcube export --out-dir ... to write reviewable bundles.yaml and bundles.secrets.yaml 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 in bundle public 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 bundle config/secrets change, use kdcube bundle reload <bundle_id> or reapply seed bundle descriptors with kdcube bundle config apply --descriptors-location ... --reload.
Reference recipe: ngrok-README.md.
Bundle-Scoped @venv(...) Helpers
Bundles can mark dependency-heavy helper functions with @venv(...). The platform creates a cached per-bundle subprocess venv, overlays the runtime packages already present in proc, then installs the bundle'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 bundle reload path.
Bundle-Scheduled @cron(...) Jobs
Bundles can now declare proc-owned scheduled jobs directly in bundle code with @cron(...). This is the new native way to bind periodic custom bundle logic to the platform lifecycle instead of wiring an external scheduler for every bundle-specific 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 bundle-owned record first, then writes a ready-work envelope to Redis Streams. Proc claims jobs fairly across workers, builds a bundle runtime context, and invokes the bundle'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 bundle-owned execution/result state. Until proc acknowledges the stream message, retry is possible.
Example Bundles
| Bundle ID | What It Shows | Key Features |
|---|---|---|
versatile@2026-03-31-13-36 | Reference bundle for SDK and integrations docs | ReAct agent, bundle-local tools, MCP tools, widget decorators, integrations operations, Telegram hook/webapp and attachment patterns, custom main UI under ui/main/, widget UI under ui/widgets/, and economics-aware entrypoint patterns |
versatile — widgets, Telegram, operations, and custom main-view pattern
The versatile sample shows how one bundle can combine a standard ReAct conversation workflow with decorated widget endpoints, bundle 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, tools_descriptor.py, ui/main/, and ui/widgets/ fit together.
Documentation Reference
SDK & Bundle
- bundle-index-README.md
- bundle-developer-guide-README.md
- bundle-agent-integration-README.md
- versatile-reference-bundle-README.md
- bundle-client-ui-README.md
- bundle-client-communication-README.md
- bundle-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
ReAct Agent [full folder →]
- flow-README.md
- react-context-README.md
- timeline-README.md
- react-turn-workspace-README.md
- agent-workspace-collaboration-README.md
- files-vs-outputs-README.md
- workspace-checkout-model-README.md
- artifact-discovery-README.md
- artifact-storage-README.md
- runtime-configuration-README.md
- source-pool-README.md
- turn-log-README.md
- turn-data-README.md
- external-exec-README.md
- event-blocks-README.md
- tool-call-blocks-README.md
- conversation-artifacts-README.md
- event-subsystem-README.md
- external-events-README.md
- event-source-README.md
- block-production-README.md
Coming Soon & Current Status
Here's the current state of platform capabilities and what's next on the roadmap:
| Feature | Status | Notes |
|---|---|---|
| Bundle-declared widgets | Available | Bundles now 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 bundle-defined widget launchers directly from discovered metadata instead of hardcoded platform lists. |
| Custom main view | Available | Bundles can override the default chat surface with a custom main UI via the reserved ui.main_view config and a discovered @ui_main entrypoint. This is not limited to static dashboards: a bundle main view can be a full SPA using REST, SSE streaming, widgets, downloads, and any richer client behavior the bundle wants to implement. |
| Static asset serving from bundle | Available | Bundle-scoped static files can now be served directly via GET /api/integrations/static/{tenant}/{project}/{bundle_id}/{path}. GET operations are also available for idempotent bundle APIs under /operations/{operation}. |
| Anonymous public bundle APIs | Available | Bundle APIs can now be exposed under /public/{operation} through @api(..., route="public"). Public methods must declare public_auth; current built-in modes are explicit open access ("none") and shared-secret header verification for webhook providers such as Telegram. |
| Bundle-served MCP endpoints | Available | Bundles can expose MCP-native HTTP endpoints through @mcp(...). Proc routes them under /mcp/{alias} or /public/mcp/{alias} and forwards the raw request into the returned FastMCP or ASGI subapp, but the bundle owns MCP authentication and authorization. |
| Bundle endpoints RBAC | Available | Bundle endpoint visibility and access are already governed by decorator metadata where proc owns the route contract. Bundle listing honors bundle-level allowed_roles, while per-method user_types and raw roles on @api and @ui_widget control which operations and widgets are visible and callable for a given user. user_types use threshold semantics with the order anonymous < registered < paid < privileged. If both are present, both checks must pass. Public API endpoints additionally require explicit public_auth. @mcp does not use proc-side user_types, roles, or public_auth. |
| Admin bundle interface enrichment | Available | /api/admin/integrations/bundles now returns registry metadata enriched with scanned bundle manifest data such as widgets, apis, on_message, on_job, and scheduled jobs. This gives admins a control-plane view of what a bundle exposes without opening the bundle code. |
| Bundle marketplace | Soon | Browse and install community bundles from a registry. |
| Local bundle dev fast prototyping | Available | Descriptor-driven local development now has a first-class fast path. Point assembly.yaml at paths.host_bundles_path, define the bundle in bundles.yaml with a container path under /bundles, initialize once with kdcube init --descriptors-location ... --workdir ..., then iterate with kdcube bundle reload <bundle_id> --workdir <runtime-workdir>. The live local deployment-scoped bundle authority is the staged runtime copy under workdir/config/, not the external source directory. See how-to-configure-and-run-bundle-README.md. |
| Live bundle reload | Available | Bundle code is loaded per-process and cached as a singleton, but local development no longer depends on restarting proc. Config overrides propagate immediately via Redis, and descriptor-backed local changes can be picked up with kdcube bundle reload <bundle_id> --workdir <runtime-workdir>, which replays the active staged descriptor and clears the bundle cache for the next request. Bundle Admin updates to deployment-scoped bundle props and bundle secrets persist into the live runtime authority first, then reload applies that authority. |
on_props_changed(...) lifecycle hook |
Available | BaseEntrypoint exposes on_props_changed(...) for reconciling long-lived side effects after effective bundle props changed. It runs after prop refresh when effective props actually changed, and already-loaded singleton bundles in the current worker receive it on live bundles.props.update events. |
| Bundle versioning | Available | Update the ref (branch/tag/commit) in bundles.yaml or via the Admin API and the change applies immediately to new requests. Redis is the runtime cache, not the long-term authority. In the recommended deployment model, deployment-scoped bundle descriptors and non-secret bundle props persist in writable bundles.yaml, and bundle export reconstructs that file. See bundle-runtime-configuration-and-secrets-README.md. |
| Deployment-scoped bundle props and secrets | Available | Bundles read deployment-scoped non-secret config with self.bundle_prop(...) and deployment-scoped bundle secrets with await get_secret_async("b:..."). Deployment-scoped bundle props are exportable and belong in bundles.yaml; deployment-scoped bundle secrets belong in bundles.secrets.yaml or the configured secrets provider. See bundle-runtime-configuration-and-secrets-README.md. |
| User-scoped bundle state | Available | Bundles can persist per-user non-secret state with get_user_prop(...) / set_user_prop(...) and per-user secrets with await get_user_secret_async(...) / await set_user_secret_async(...). User-scoped state is operational runtime data. It never belongs in bundles.yaml, bundles.secrets.yaml, or kdcube export. See bundle-runtime-configuration-and-secrets-README.md. |
| Runtime feature gating and configurable visibility | Available | Bundles can gate the whole bundle through the canonical enabled.bundle prop, expose Admin-managed resource enabled flags, and make bundle/API/widget visibility configurable 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. |
Bundle-scoped @venv helpers |
Available | Bundles can run selected dependency-heavy helper functions in a cached per-bundle subprocess venv. The venv inherits the platform runtime packages, installs the bundle's requirements.txt on top, and rebuilds only when the requirements hash changes. This is intended for leaf helper work, not for communicator, DB, Redis, or other live proc-bound runtime objects. |
| Bundle-local Node / TS backend sidecar | Available | Bundles can keep the public KDCube app surface in Python and run selected backend logic as a bundle-local Node or TypeScript sidecar. This is the supported wrapping pattern for existing Node backends. Startup-config drift triggers lazy restart on next use; live config can be pushed lazily through POST /__kdcube/reconfigure. |
Bundle-scheduled @cron jobs |
Available | Bundles can now declare scheduled jobs directly in bundle code with @cron(alias=..., cron_expression=..., expr_config=..., span=...). The scheduler resolves expr_config first, falls back to inline cron_expression only when no config path is declared, and supports process, instance, and system exclusivity spans. |
Bundle background @on_job handlers |
Available | Bundles can declare one async @on_job handler for ready work claimed by proc from the Redis background job stream. This is the execution side of queued background work: @cron or a run-now operation can enqueue, while @on_job validates work_kind, reads ids from payload, and updates bundle-owned result state. |
| Git-backed React workspace | Available | React now supports a git workspace backend with sparse current-turn repos, lineage-isolated history, explicit materialization through react.pull(...), and active workspace construction through react.checkout(mode="replace"|"overlay", ...). The agent-facing contract is the same in custom and git modes: files/ is editable workspace state, outputs/ and snapshots/ are artifacts, cross-conversation refs use fi:conv_..., and custom namespaces such as ext: must be rehosted by react.pull before normal file tools can use them. |
| Bundle event sources and artifact rehosters | Available | Tools and authored UI events can be declared as event sources with phase-bound policies. block_production policies let tools and external events produce the timeline blocks/artifact rows they own, while registered namespace rehosters let react.pull materialize refs such as ext:... into ordinary fi: workspace paths. |
| Per-bundle React runtime instructions | Available | Bundles can append deploy-scoped prompt guidance to the React decision agent through react.additional_instructions. This is useful for bundle-specific style, output-format, or domain guardrails without forking the base system prompt. |
| Claude Code agent integration | Available | Bundles can now 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. The kdcube.copilot reference bundle shows the pattern end to end for admin-facing workflows. |
| Interactive steer / followup execution | Available | Busy-turn followup and steer are implemented end to end for React. They enter the shared conversation event lane, the live React runtime folds them into the current timeline when it owns the turn, followup continues the current turn, and steer redirects at the next safe checkpoint. Authored bundle events use the same lane identity model with explicit agent_id, event_source_id, and routing.reactive. |
| KDCube bundle-copilot | Partial | The bundle-copilot foundation already exists as a React-based coding bundle and can help scaffold or evolve bundle code. The remaining work is to complete the product experience for bundle builders, including smoother workspace continuity, stronger guidance, and tighter bundle-development flows. |
| Multi-bundle conversations | Partial | Technically possible today via routing, but both bundles must understand and agree on the shared conversation format (timeline structure, artifact paths, turn state). Requires alignment between bundle developers on the protocol. |
| Streaming for operations | Available | Bundle operations can already stream to connected clients. REST calls can target the initiating peer with KDC-Stream-ID, and bundle code can emit deltas, steps, and widget updates through the communicator over SSE or Socket.IO while the operation is still running. |
| Claude Code agent: isolated tools, KDCube skills, bundle tools | Soon | The next Claude Code step is deeper platform-native integration: exposing controlled isolated tools, KDCube skills, and bundle-defined tools inside the Claude Code runtime so bundles can combine Claude's coding loop with the same governed tool surface used elsewhere in the SDK. |
| Codex agent integration | Soon | Codex integration is planned as another first-class agent runtime in the platform. The goal is the same accountable, bundle-integrated, UI-visible operating model we now have for Claude Code, but adapted to Codex-specific execution and tool semantics. |
| Browser navigation toolset for agents | Soon | A richer browser-navigation tool family for agentic use is the next natural step beyond search and fetch. The goal is to let bundles navigate, inspect, and operate on web flows more deliberately while still staying inside the platform’s controlled tool surface. |
| Policy DSL | Roadmap | Relevant for regulated or security-sensitive deployments where operators want to express bundle access restrictions, tenant rules, and data-handling constraints declaratively instead of encoding them in Python hooks. Important governance work, but not on the critical path for the current bundle interface rollout. |
| Deterministic Enforcement Engine | Roadmap | Highly complementary to policy work: enforcement decisions should stay deterministic, auditable, and separate from LLM judgment. This is a strong control-plane capability for the platform, especially once policies, gates, and richer workflow contracts grow. |
| Workflow Invariants | Roadmap | One of the most immediately relevant roadmap items for agent reliability. Declarative assertions such as “artifact Z must exist before LLM call” or “tool X may only run after step Y” would let the runtime halt bad states before they propagate. |
| Cross-Agent Approval Gates | Roadmap | Relevant when multi-agent or supervisor-driven workflows become a primary product surface. The idea is to let a worker or sub-agent pause on a high-impact action and require approval from a coordinator or human operator before continuing. |