Skip to content

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.yaml files 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:

ModuleExport PathPurpose
ck-client.js. / ./clientCore NATS WSS client; connection, auth, send, subscribe
ck-page.js./pagePage harness (console.html); auto-detects kernel, renders chrome
ck-bus.js./busIn-browser event bus for decoupled component communication
ck-kernel.js./kernelKernel-specific CSS variables and theming
ck-registry.js./registryKernel registry for multi-kernel page composition
ck-runtime.js./runtimeClient-side runtime utilities
ck-materializer.js./materializerClient-side resource materialisation
ck-store.js./storeClient-side state persistence (localStorage + server filer)
ck-shapes.js./shapesSHACL shape rendering and validation UI
ck-anim.js./animAnimation engine for kernel visualisations
ck-anim-grammar.js(internal)Animation grammar parser
ck-sound.js./soundWeb 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:

javascript
const ck = new CKClient({
  kernel: "Delvinator.Core",
  wssEndpoint: "wss://stream.tech.games",
  authEndpoint: "https://id.tech.games",
  realm: "techgames",
  clientId: "ck-web"
})
MethodReturnsDescription
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()voidDowngrade to anonymous identity
disconnect()Promise<void>Unsubscribe all, drain connection
on(event, fn)voidSubscribe to: "result", "event", "status", "error"

Action Dispatch Flow

When a user clicks an action in the web shell:

  1. The parameter form populates with a JSON template containing the action's expected parameters
  2. The user fills values and clicks send
  3. ck-client.js publishes to input.{kernel} with full NATS headers (Trace-Id, X-User-ID, Authorization)
  4. The kernel processor receives the message, executes the action
  5. Results arrive on result.{kernel} and/or stream.{kernel}
  6. The results panel renders the response in the selected view mode

All messages published by CKClient include these NATS headers:

HeaderValuePurpose
Nats-Msg-IdClient-generated unique IDDeduplication
Trace-Idtx-{hex6}Distributed trace correlation
X-Kernel-IDClient instance IDSender identification
X-User-IDUser ID (anonymous or authenticated)User attribution
AuthorizationBearer {jwt} (if authenticated)JWT for server-side verification

Three-Panel Layout

PanelWidthContent
Action sidebar160pxKernel tabs, action buttons with lock/unlock icons, NATS topic list
Parameter form280pxPer-action input fields, JSON preview, send button
Resultsflex (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) for access: auth actions when not authenticated
  • Unlock icon (lock_open) for actions available to the current user
  • Kernel type icon: admin_panel_settings for operator-type, memory for 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:

ModeShowsUse Case
chatProgressive text bubbles with streaming supportInteractive conversation with Claude-backed actions
bodyRaw JSON payload, syntax highlightedDebugging, inspecting response structure
envelopeFull NATS headers + body + trace-idProtocol-level debugging

Config Resolution

The console resolves its configuration through a three-step fallback (v3.5.6.1):

  1. window.__CK_CONFIG -- inline config from generated index.html
  2. fetch('/') and parse response headers -- for CDN or proxy setups
  3. 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:

  1. Login button in the header -- triggers Keycloak password grant
  2. JWT stored in memory -- never in localStorage (XSS mitigation)
  3. Auto-refresh -- 30 seconds before JWT expiry, ck-client.js silently refreshes
  4. Action gating -- actions with access: auth are disabled (greyed out, lock icon) when the user is anonymous
  5. 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-web public 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:

TopicContentView Mode
result.{kernel}Action results (sealed instances)body, envelope
event.{kernel}Lifecycle events (connected, ready, error)envelope
stream.{kernel}Per-token Claude streaming outputchat

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:

javascript
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 shell

The 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 -- no innerHTML
  • Dark theme: #0a0a0a background, #ff8a65 accent, #66bb6a success, monospace font

Released under the MIT License.