KDCube is built around bundles
Everything you ship on KDCube lives inside a bundle. The bundle is the single unit of deployment, versioning, tenancy, and lifecycle — every other part of the platform exists to run bundles, isolate them, meter them, and stream their output. When you install KDCube, you are installing the runtime that hosts bundles; when you build "an agent," you are authoring a bundle. The SDK includes both the built-in ReAct subsystem and a separate ISO runtime subsystem that bundles can use for controlled execution.
That single decision shapes most of the SDK's ergonomics:
- One authoring surface for the whole application. A bundle carries Python backend code, optional TypeScript UI widgets and views, tools, skills, outbound MCP tool integrations, optional bundle-served MCP endpoints, prompts, configuration, hosted file outputs, and storage layout — all in one package, versioned together.
- Hot-loadable without restarting the runtime. Bundles 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. A bundle runs against a
(tenant, project, user)context the platform resolves at admission. Storage paths, budgets, decision logs, and streaming channels inherit that scope — the bundle doesn't have to enforce it manually. - Metered and billed as a unit. Every model call, tool invocation, and storage write routed through the bundle is tagged for economics accounting, so per-tenant spend, audit, and rate limits apply without extra plumbing.
- Framework-agnostic inside. A bundle 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 bundle 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 bundles inside one tenant/project when they belong to the same environment.
The rest of this page is a tour of how you actually build one: what a bundle contains, how it plugs into the platform, what the built-in primitives (storage, tools, hosted files, SDK integrations, widgets, APIs, @mcp, @cron, @on_job, @venv) look like, and how you deploy it.
What Is a Bundle?
A Bundle 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 bundle by subclassing the platform's base class (BaseEntrypoint or a derived variant) and registering with @agentic_workflow.
A bundle 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, deployment-scoped bundle config, deployment-scoped bundle secrets, and optional per-user state. One environment can host many such bundles at the same time.
Bundle UI is a platform-served surface, not a special iframe concept. A bundle 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 a bundle object.
Bundle code can be delivered either from git or from a mounted local bundle path. In deployed environments the registry usually points at a git repo, ref, and subdirectory. In local descriptor-driven development, use kdcube init --descriptors-location ... to stage a concrete runtime under --workdir, then iterate with kdcube reload <bundle_id> --workdir <runtime-workdir>.
Most example bundles use the ReAct agent. The current runtime features ReAct v3, selected through AI_REACT_AGENT_VERSION=v3. The ReAct subsystem gives bundles tools, skills, continuous conversations, a sources pool, ANNOUNCE, a signals board, and an event timeline that is not limited to tool calls. 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 bundle with config.react.max_iterations or react.max_iterations in bundle props. A bundle 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.
Example Bundles
Start from the examples folder in sdk/examples/bundles/. The concrete reference bundle used on this page is versatile@2026-03-31-13-36 — a production-style sample that combines ReAct orchestration, bundle-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 bundle for bundle authoring docs, but not the reference for @cron or @venv.
| Bundle | Description |
|---|---|
versatile@2026-03-31-13-36 |
Reference bundle for this page. Demonstrates ReAct orchestration, bundle-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 Bundle 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.
A Bundle is your unit of deployment. It's a Python package that plugs into the platform and defines how your AI agent thinks, what tools it uses, how it stores data, what UI widgets it exposes, and how it bills users.
Big Picture: How It All Fits Together
Two Services, One Platform
Ingress (chat-ingress)
Handles all inbound traffic. Authenticates users, enforces rate limits, validates the bundle, enqueues the task, and opens an SSE stream back to the client. Your bundle rarely needs to know about this — the platform wires it up automatically.
Processor (chat-proc)
Dequeues tasks, loads your bundle singleton, and calls execute_core(). It also hosts the Operations API — the REST endpoint that your UI widgets call directly (no SSE needed for widget interactions).
Bundle Anatomy
A bundle is a Python package with a required entrypoint.py and optional supporting files:
Minimal Entrypoint
from kdcube_ai_app.infra.plugin.agentic_loader import agentic_workflow
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"
@agentic_workflow(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"
}
}
}
def on_bundle_load(self, **kwargs):
# Called ONCE per process when bundle first loads.
self.logger.info("Bundle 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 bundle!", index=0, marker="answer")
return {"final_answer": "Done", "followups": []}
BaseEntrypoint Lifecycle
| Method / Property | When Called | Typical Use |
|---|---|---|
configuration (property) | On bundle 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 bundle props changed | Reconcile long-lived side effects after prop refresh |
pre_run_hook(state) | Before each turn | Re-reconcile knowledge space if config changed |
execute_core(state, thread_id, params) | Every turn | Build + invoke your LangGraph workflow |
rebind_request_context(...) | On cached singleton reuse | Refresh request-bound state (comm, user, etc.) |
Bundle Lifecycle
A bundle 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
Bundle source can come from git or from a mounted local bundle path. The platform resolves the configured bundle source, imports the module, and finds the class decorated with @agentic_workflow. The loader extracts interface metadata (decorators for widgets, APIs, message handler, and background job handler) and builds a BundleInterfaceManifest 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 bundle 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 bundle storage, Redis, or bundle props — not on self. If a bundle needs instance-local filesystem state, it should resolve that location through the platform bundle-storage helper instead of creating ad hoc runtime folders next to bundle source code. Singleton reuse is keyed by the loaded bundle 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 bundle logic (chat turn or operation handling) |
post_run_hook(state, result) | Every invocation | Final bookkeeping after execution completes |
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 bundle 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 bundle 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 bundle props. Do not use it for request-local validation or heavy one-time install/build work.
Hot-Reload on Config Change
When you update a bundle's ref in bundles.yaml and re-apply, the platform detects the configuration change via a content hash of the bundle directory. The updated bundle 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 bundle instance refreshes effective props on the next invocation, and on_props_changed(...) fires only if the effective props actually changed. Already-loaded singleton bundles 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 bundle 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-live-bundles; 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-live-bundles; 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 | Bundle feature flags, cron config, model selection, UI config | Exported to bundles.yaml; included in kdcube --export-live-bundles |
| Bundle 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 | Bundle-scoped webhook secrets and shared API tokens | Exported to bundles.secrets.yaml when the provider/export flow can reconstruct bundle secrets |
| User bundle 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 bundle export |
| User bundle 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 bundle export |
| Redis KV cache | developer-defined read/write keys | Redis only | Lightweight distributed state, flags, small caches | No descriptor export |
| Bundle storage backend | bundle 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 bundle state belongs to bundle descriptors and bundle export. Platform/global deployment state and all user-scoped state stay outside kdcube --export-live-bundles.
For isolated execution, Docker/Fargate supervisors receive descriptor payloads and materialize them before tool bootstrap. Bundle tools therefore read the same platform props, platform secrets, bundle props, and bundle 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 a bundle 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 bundle from the bound runtime context. During normal bundle execution that context is already present; outside a bound request or bundle runtime, use a fully qualified secret path instead of b:. Use the async helpers in new bundle APIs, tools, cron handlers, and @on_job handlers; sync secret helpers are compatibility APIs for old sync-only code.
Node / TypeScript Backend Inside a Bundle
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 a bundle-local sidecar.
That split is intentional:
- Python bundle owns decorators, auth, role gating, bundle props, bundle 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 bundle spec and
tenant/projectscope - bundle 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 bundle shell. The bundle remains the KDCube application unit; Node is one internal implementation part of that bundle.
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": "..."})
REST Operations (for UI Widgets)
Your bundle exposes additional REST operations via the Operations API hosted by the Processor:
POST /bundles/{tenant}/{project}/{bundle_id}/operations/{operation}
GET /bundles/{tenant}/{project}/{bundle_id}/operations/{operation}
The explicit bundle_id form is now 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 /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.
@agentic_workflow(
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={"type": "emoji", "value": "⚙️"},
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 a shared conversation external-event source. A live React turn can consume them immediately: followup stays on the current turn, while steer interrupts the current turn at the next safe checkpoint. If there is no live owner, the processor later promotes the pending event into a normal next turn. |
followup and steer. A busy conversation now accepts these into the shared external-event source rather than dropping them. followup is a live “continue on this turn” input, while steer is a live “interrupt and redirect this turn” signal. When React owns the turn it consumes these directly on the current timeline; only unconsumed events are later promoted into a normal next turn.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 (AIBundleStorage)
from kdcube_ai_app.apps.chat.sdk.storage.ai_bundle_storage import AIBundleStorage
storage = AIBundleStorage(
tenant="my-tenant", project="my-project",
ai_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 historical artifacts from fi: refs as readonly local reference material; subtree pulls for .files/..., exact-file pulls for .outputs/... and attachments. 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:<turn>.files/... refs. mode="replace" seeds the workspace from scratch; mode="overlay" imports selected historical files into the existing workspace. |
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 |
Bundle-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.
# 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},
# Bundle-local tools ("ref" = path relative to bundle 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 bundle/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.
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 bundle props.
# 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:
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.
Artifact Path Families
| Prefix | Resolves To | Example |
|---|---|---|
fi: | Turn-scoped file artifact namespace for workspace files, non-workspace outputs, and attachments | fi:turn_123.outputs/export/report.pdf |
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.
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 |
Custom Bundle Skill
# skills/my_skill/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/my_skill/tools.yaml
tools:
- id: product_search
role: search
why: Search the product catalog
# skills_descriptor.py
AGENTS_CONFIG = {
"solver": {
"enabled_skills": ["my_skill", "pdf-press", "url-gen"],
"disabled_skills": []
}
}
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/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: []
---
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 tools for the skill, used by planners and UX
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.decision.v2": {
"enabled": ["product.kdcube"]
},
# 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 filtering — all registered skills are visible |
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 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 - Use
AGENTS_CONFIGto restrict skill visibility rather than deleting skill files — this keeps skills available for future consumers - Test visibility filtering per consumer: a skill hidden from
solver.react.decision.v2is still visible to unfiltered consumers
Widgets, APIs, MCP & Custom UI
Bundles now expose a decorator-discovered interface. Widgets, REST APIs, bundle-served MCP endpoints, chat message handlers, 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. A bundle 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.web_app_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:
web_app_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>.
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.
Bundle Interface Decorators
| Decorator | Attaches To | Purpose | Key args |
|---|---|---|---|
@agentic_workflow(...) | class | Marks the bundle entrypoint for loader discovery | name, version, priority, allowed_roles, allowed_roles_config |
@bundle_id(...) | class | Declares the canonical bundle 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 a bundle 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 a bundle-served MCP endpoint under /mcp/{alias} or /public/mcp/{alias} | alias, route, transport |
@ui_main | method | Marks the bundle’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 job |
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 @agentic_workflow 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.
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.agentic_loader import agentic_workflow, bundle_id, api, mcp, ui_widget, ui_main, on_message
@agentic_workflow(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 bundle props config.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. It should send CONFIG_REQUEST to the parent frame when embedded by the standard KDCube shell, accept both CONN_RESPONSE and CONFIG_RESPONSE, and 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.
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.
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 (base URL, auth tokens, tenant/project) via postMessage from the host frame. 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.
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_bundle | Bundle admin dashboard (props editor, status) — all bundles inherit this |
control_plane | Economics dashboard (usage, billing) — via BaseEntrypointWithEconomics |
suggestions | Suggested prompts for new conversations |
task-tracker-api | Example structured API for a task board widget: list tasks, create tasks, schedule runs, 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 \ --workdir ~/.kdcube/kdcube-runtime \ --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 --workdir ~/.kdcube/kdcube-runtime/<tenant>__<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 Bundle Dashboard (
/api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/ai_bundle). 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 workflow optimized for fast iteration. 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 \
--workdir ~/.kdcube/kdcube-runtime \
--descriptors-location /path/to/descriptors \
--set-secret services.openai.api_key "sk-..." \
--set-secret services.anthropic.api_key "sk-ant-..."
kdcube start --workdir ~/.kdcube/kdcube-runtime/<tenant>__<project>
# then during development
# edit files under assembly.paths.host_bundles_path
kdcube reload my.bundle@1.0.0 --workdir ~/.kdcube/kdcube-runtime/<tenant>__<project>
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. reload replays that active staged descriptor, clears bundle caches, and makes the next request load the updated bundle code.
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 --workdir ~/.kdcube/kdcube-runtime/<tenant>__<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 stop and kdcube start; if bundle config/secrets change, use kdcube reload <bundle_id>.
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.agentic_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.agentic_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 task-tracker routine that materializes the next due queue and publishes an operational event for the UI:
class TaskTrackerBundle(BaseEntrypoint):
@cron(alias="refresh-task-queue", cron_expression="0 6 * * *", span="system")
async def refresh_task_queue(self):
tasks = await self.rebuild_due_task_queue()
await self.comm.event(
type="bundle.tasks.queue_refreshed",
step="refresh_task_queue",
status="completed",
title="Task queue refreshed",
data={"items": len(tasks)},
)
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.agentic_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-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
- 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
- agent-workspace-collaboration-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 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 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-live-bundles. 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 historical materialization via react.pull(...), and active workspace construction via react.checkout(mode="replace"|"overlay", ...). Turn-end publish writes current-turn text workspace state back into the lineage without exposing other users' branches. |
| 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 now implemented end to end for React. Ingress writes them into the shared external-event source, the live React runtime folds them into the current timeline when it owns the turn, followup continues the current turn, and steer interrupts at the next safe checkpoint. If no live owner consumes them, the processor promotes them into a normal next turn. |
| 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. |