Web Shell
Why a Kernel-Driven UI
Most admin consoles are built top-down: a developer designs the UI, then wires it to an API. CKP inverts this. The web shell is generated by the operator and populated by the kernels. The UI has no hardcoded knowledge of what actions exist or what parameters they take. It discovers everything at runtime from the kernel declarations.
This matters because:
- When a new action is added to a kernel's
conceptkernel.yaml, it appears in the web shell automatically - When a new kernel joins the project, its actions appear in the sidebar on the next status refresh
- The web shell does not need to be redeployed when kernels evolve
The shell is a thin rendering layer over NATS. It publishes messages to input.{kernel}, subscribes to result.{kernel} and stream.{kernel}, and renders whatever comes back.
Architecture
CK.Operator generates a minimal index.html (~30 lines) via the deploy.web reconciliation step. This bootstraps the full web shell from CK.Lib.Js files on the CK volume:
index.html (generated by CK.Operator deploy.web)
|-- loads /cklib/ck-client.js (NATS WSS client + auth)
|-- loads /cklib/ck-page.js (page harness)
|-- loads /cklib/console.js (UI logic)
|-- loads /cklib/console.css (dark theme styles)
|-- passes inline config via window.__CK_CONFIG:
{
kernels: [...], // from conceptkernel.yaml scan
auth: {...}, // from project AuthConfig
nats: { wss: "..." } // NATS WebSocket endpoint
}
|
console.js builds:
|-- Header (project name, connection status, login/logout)
|-- Action sidebar (from kernel spec.actions)
|-- Parameter form (per action)
|-- Results panel (NATS messages in three view modes)Why index.html is Generated, Not Static
The operator generates index.html because it contains project-specific configuration that must match the deployed state:
- The kernel list comes from scanning
conceptkernel.yamlfiles during reconciliation - The auth endpoint comes from the project's
AuthConfig - The NATS WSS URL comes from the cluster's NATS gateway configuration
If index.html were static, any of these values could drift from the actual cluster state. By generating it during deploy.web, the operator guarantees the HTML matches reality.
CK.Lib.Js Module Catalog
The actual UI logic lives in CK.Lib.Js -- a system kernel that provides shared JavaScript libraries. The full module catalog includes 12 modules:
| Module | Export Path | Purpose |
|---|---|---|
ck-client.js | . / ./client | Core NATS WSS client; connection, auth, send, subscribe |
ck-page.js | ./page | Page harness (console.html); auto-detects kernel, renders chrome |
ck-bus.js | ./bus | In-browser event bus for decoupled component communication |
ck-kernel.js | ./kernel | Kernel-specific CSS variables and theming |
ck-registry.js | ./registry | Kernel registry for multi-kernel page composition |
ck-runtime.js | ./runtime | Client-side runtime utilities |
ck-materializer.js | ./materializer | Client-side resource materialisation |
ck-store.js | ./store | Client-side state persistence (localStorage + server filer) |
ck-shapes.js | ./shapes | SHACL shape rendering and validation UI |
ck-anim.js | ./anim | Animation engine for kernel visualisations |
ck-anim-grammar.js | (internal) | Animation grammar parser |
ck-sound.js | ./sound | Web Audio API integration for kernel sound effects |
These files are served from the CK volume at /cklib/, mounted via a COMPOSES edge to CK.Lib.Js. No CDN, no npm install, no build step. The operator mounts the volume; the gateway serves the files.
CKClient API
CKClient is the core NATS WSS client within ck-client.js. It manages connection lifecycle, authentication escalation, and message send/receive:
const ck = new CKClient({
kernel: "Delvinator.Core",
wssEndpoint: "wss://stream.tech.games",
authEndpoint: "https://id.tech.games",
realm: "techgames",
clientId: "ck-web"
})| Method | Returns | Description |
|---|---|---|
connect() | Promise<boolean> | Connect to NATS WSS, auto-subscribe to result.{kernel} and event.{kernel} |
send(data) | Promise<string> | Publish to input.{kernel}; returns traceId |
login(username, password) | Promise<string> | Keycloak password grant; returns userId |
logout() | void | Downgrade to anonymous identity |
disconnect() | Promise<void> | Unsubscribe all, drain connection |
on(event, fn) | void | Subscribe to: "result", "event", "status", "error" |
Action Dispatch Flow
When a user clicks an action in the web shell:
- The parameter form populates with a JSON template containing the action's expected parameters
- The user fills values and clicks send
ck-client.jspublishes toinput.{kernel}with full NATS headers (Trace-Id, X-User-ID, Authorization)- The kernel processor receives the message, executes the action
- Results arrive on
result.{kernel}and/orstream.{kernel} - The results panel renders the response in the selected view mode
All messages published by CKClient include these NATS headers:
| Header | Value | Purpose |
|---|---|---|
Nats-Msg-Id | Client-generated unique ID | Deduplication |
Trace-Id | tx-{hex6} | Distributed trace correlation |
X-Kernel-ID | Client instance ID | Sender identification |
X-User-ID | User ID (anonymous or authenticated) | User attribution |
Authorization | Bearer {jwt} (if authenticated) | JWT for server-side verification |
Three-Panel Layout
| Panel | Width | Content |
|---|---|---|
| Action sidebar | 160px | Kernel tabs, action buttons with lock/unlock icons, NATS topic list |
| Parameter form | 280px | Per-action input fields, JSON preview, send button |
| Results | flex (fills remaining space) | NATS messages in chat, body, or envelope mode |
Action Sidebar
The sidebar is built by sending {action: "status"} to each kernel on boot. The response includes the kernel's action list with access levels. Each action shows:
- Action name as a button
- Lock icon (Material Icons
lock) foraccess: authactions when not authenticated - Unlock icon (
lock_open) for actions available to the current user - Kernel type icon:
admin_panel_settingsfor operator-type,memoryfor domain-type
When the user clicks an action, the parameter form populates with a JSON template containing the action's expected parameters. The user fills the values and clicks send.
Results Panel
Three view modes for received messages:
| Mode | Shows | Use Case |
|---|---|---|
| chat | Progressive text bubbles with streaming support | Interactive conversation with Claude-backed actions |
| body | Raw JSON payload, syntax highlighted | Debugging, inspecting response structure |
| envelope | Full NATS headers + body + trace-id | Protocol-level debugging |
Config Resolution
The console resolves its configuration through a three-step fallback (v3.5.6.1):
window.__CK_CONFIG-- inline config from generatedindex.htmlfetch('/')and parse response headers -- for CDN or proxy setups- URL parameters --
?nats=wss://...&realm=...-- for development
This ensures the shell works in production (operator-generated config), behind a proxy (header-based config), and during local development (URL params).
Auth Integration
The web shell integrates with Keycloak through ck-client.js:
- Login button in the header -- triggers Keycloak password grant
- JWT stored in memory -- never in localStorage (XSS mitigation)
- Auto-refresh -- 30 seconds before JWT expiry, ck-client.js silently refreshes
- Action gating -- actions with
access: authare disabled (greyed out, lock icon) when the user is anonymous - Bearer token -- attached to all NATS message headers after login
Why Password Grant, Not Authorization Code Flow?
The web shell uses the Keycloak direct access (password) grant because:
- The shell is a single-page app with no backend server to handle auth code callbacks
- NATS WSS does not support redirect flows
- The password grant is scoped to the
ck-webpublic client with limited permissions - Future versions may migrate to PKCE authorization code flow when NATS adds support
Security Consideration
The direct access grant sends credentials to the Keycloak endpoint directly from the browser. This is acceptable for internal/development deployments where the Keycloak instance is trusted. For production internet-facing deployments, PKCE authorization code flow with a BFF (backend-for-frontend) pattern is recommended.
NATS Subscriptions
Per kernel in the project, the web shell subscribes to:
| Topic | Content | View Mode |
|---|---|---|
result.{kernel} | Action results (sealed instances) | body, envelope |
event.{kernel} | Lifecycle events (connected, ready, error) | envelope |
stream.{kernel} | Per-token Claude streaming output | chat |
The stream.{kernel} subscription is what enables progressive rendering of Claude responses. When a kernel processes an LLM-backed action, each token arrives as a separate NATS message. The chat view mode renders them as a growing text bubble. See Streaming for the full event model.
DOM Construction
All DOM elements are built via an el() helper function -- no inline HTML, no innerHTML assignments, no template literals:
function el(tag, attrs, ...children) {
const e = document.createElement(tag);
if (attrs) Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v));
children.forEach(c => typeof c === 'string' ? e.append(c) : e.appendChild(c));
return e;
}This is a security decision. By never using innerHTML, the shell is immune to XSS injection from NATS message payloads. Every string is text-only; every element is created via the DOM API.
Kernel Subfolder Mounting
Each COMPOSES edge with web.serve: true gets an HTTPRoute subpath rule. The composed kernel's storage/web/ is served at /{edge_slug}/:
https://delvinator.tech.games/ -> CK.Operator index.html
https://delvinator.tech.games/cklib/ -> CK.Lib.Js storage/web/
https://delvinator.tech.games/cklib/console.html -> full web shellThe gateway handles this routing. The web shell itself does not know about subfolder mounting -- it loads its scripts from /cklib/ because that is what the generated index.html specifies.
Architectural Consistency Check
Logical Analysis: Web Shell and the Three-Loop Model
Question: Does the web shell violate the three-loop separation?
Answer: No. The web shell is a read-only consumer of CK loop data (it reads action declarations) and a publisher to NATS (it sends user requests). It does not write to any kernel's DATA loop directly. Results flow through NATS -- the kernel processor writes instances to the DATA loop, not the browser.
Question: Is console.js part of the CK loop or the TOOL loop?
Answer: It is part of CK.Lib.Js's DATA loop -- specifically storage/web/. CK.Lib.Js is a system kernel whose purpose is to produce shared web assets. The JavaScript files are DATA (produced output), not TOOL (executable code). This is correct: the browser downloads and executes them, but from the kernel's perspective they are static assets stored in storage/web/.
Question: Why not use a framework (React, Vue)?
Answer: The web shell is ~500 lines of vanilla JavaScript. A framework would add a build step, a node_modules dependency, and version management. The shell needs to be deployable via file upload to SeaweedFS -- no build pipeline. The el() helper provides enough DOM abstraction. The simplicity is the feature.
Gap identified: The web shell currently has no offline support. If NATS WSS disconnects, the shell shows a status chip but does not queue messages for retry. Actions sent during disconnection are lost. This is acceptable for v3.6 but should be addressed if the shell becomes the primary operational interface.
Conformance Requirements
- CK.Operator MUST generate index.html that loads CK.Lib.Js from the CK volume
- The web shell MUST build its action sidebar from the kernel's declared actions
- The web shell MUST support anonymous and authenticated modes
- Auth config (issuer, realm, client_id) MUST be injected by the operator, not hardcoded
- All DOM construction MUST use the
el()helper or DOM API -- noinnerHTML - Dark theme:
#0a0a0abackground,#ff8a65accent,#66bb6asuccess, monospace font