2.1 Seven Kernel Responsibilities
The Kalos kernel provides exactly seven services. All application behavior — including domain modeling, authentication, data persistence, HTTP handling, and scripting — is implemented by plugins.
1. Configuration. The kernel loads a JSON configuration file at startup, resolves environment variable substitutions, and exposes the configuration tree to plugins through the ICore interface. See §3 for the configuration schema.
2. Plugin Loading. The kernel reads the plugins array from the configuration file and loads each plugin as a shared library via dlopen (POSIX) or LoadLibrary (Windows). Plugins are loaded in the order declared in the configuration. The kernel does not resolve plugin dependencies — the configuration author MUST order plugins so that each plugin’s dependencies are loaded before it.
3. Message Bus. The kernel maintains an action registry: a map from action names (strings) to handler functions. Plugins register actions during their lifecycle. When an action is dispatched, the kernel invokes the registered handler and returns the response. The bus is synchronous — dispatch blocks until the handler returns.
4. Lifecycle Management. The kernel drives each plugin through a five-phase lifecycle (§2.3). It handles startup sequencing, graceful shutdown, and signal handling (SIGINT, SIGTERM on POSIX; Ctrl+C on Windows).
5. Logging. The kernel provides a structured logging facility via spdlog. Plugins access the logger through ICore::logger(). Log levels (trace, debug, info, warn, error, critical) are configured globally and MAY be overridden per plugin in future versions.
6. Plugin Registry. The kernel maintains a registry of all loaded plugins with their names, versions, and archetypes. This registry is queryable through the management socket and the admin API.
7. Management Socket. The kernel opens a Unix domain socket (POSIX) or named pipe (Windows) for out-of-band administration. Commands include status queries, plugin enumeration, and graceful shutdown. The management socket is not exposed to HTTP clients.
The kernel MUST NOT implement any behavior beyond these seven services. Domain logic, transport logic, storage logic, and authentication logic MUST be implemented as plugins.
2.2 The ICore Interface
Every plugin receives a pointer to the ICore interface during registration. ICore is the plugin’s sole gateway to kernel services. A plugin MUST NOT access kernel internals, global state, or other plugins except through ICore.
ICore exposes the following capabilities:
| Method | Returns | Purpose |
|---|---|---|
config() | IConfig* | Read-only access to the configuration tree |
logger() | spdlog::logger* | Structured logging |
register_action(name, handler) | void | Register a named action on the message bus |
handle_request(request) | Response | Dispatch a request to a registered action handler |
register_route(method, path, action) | void | Map an HTTP route to a bus action (consumed by the transport plugin) |
get_store(name) | IStore* | Access a named store plugin instance |
get_plugin(name) | IPlugin* | Access a named plugin instance |
emit(event, data) | void | Emit a bus event for subscribers |
subscribe(event, callback) | void | Subscribe to bus events |
Action registration is append-only. Once an action is registered, it cannot be replaced or removed during the lifetime of the process. If a plugin registers an action with a name that is already registered, the kernel MUST reject the registration and log an error. See §2.8 for the implications of this limitation.
Request dispatch via handle_request() is the primary communication mechanism between plugins. A transport plugin receives an external HTTP request, constructs a Request object (action name, parameters, headers, body, identity), and dispatches it through handle_request(). The kernel looks up the registered handler for the action name and invokes it. The handler returns a Response object (status code, body, headers). Internal callers (Lua scripts, workflow steps, scheduled tasks) use the same dispatch path.
The Request object carries:
| Field | Type | Description |
|---|---|---|
action | string | The target action name (e.g., adl.blog.post.create) |
params | json | URL parameters (:id, query string values) |
body | json | Request body (parsed JSON) |
headers | map<string,string> | HTTP headers (transport-originated requests only) |
identity | Identity | The authenticated user (or __system__ for internal requests) |
transport | string | Origin: "http", "internal", "seeder", "lua", "workflow", "scheduler" |
The Response object carries:
| Field | Type | Description |
|---|---|---|
code | integer | HTTP status code (200, 201, 204, 400, 401, 403, 404, 409, 422, 429, 500) |
data | json | Response body |
error_message | string | Human-readable error description (empty on success) |
headers | map<string,string> | Response headers (optional) |
2.3 Plugin Lifecycle
Every plugin implements five lifecycle methods. The kernel calls them in strict order during startup and shutdown.
Server start:
for each plugin in config order:
plugin.on_register(core) ← receive ICore pointer
for each plugin in config order:
plugin.on_init() ← initialize internal state
for each plugin in config order:
plugin.on_start() ← register actions, begin work
Server stop (reverse order):
for each plugin in reverse config order:
plugin.on_stop() ← stop accepting work
for each plugin in reverse config order:
plugin.on_destroy() ← release resources
on_register(core)
The kernel passes the ICore pointer to the plugin. The plugin MUST store this pointer and MUST NOT perform any initialization beyond storing it. No other plugins are guaranteed to be loaded at this point.
on_init()
The plugin initializes its internal state. Store plugins open database connections. The ADL plugin opens its registry database, restores previously-active domains, and runs boot-time schema verification (§30.7). The auth plugin initializes its token signing key and user store.
A plugin MAY dispatch requests to actions registered by plugins that appear earlier in the configuration. A plugin MUST NOT dispatch requests to actions registered by plugins that appear later — those actions do not exist yet.
If on_init() fails (throws an exception or returns an error), the kernel MUST log the error and terminate the process. A failed plugin initialization leaves the system in an undefined state.
on_start()
The plugin registers its actions on the message bus and begins accepting work. The ADL plugin registers domain management actions (adl.domain.submit, adl.domain.status, adl.domain.list) and, if configured, bootstraps a domain from a YAML file. The transport plugin begins listening for HTTP connections.
After on_start() completes for all plugins, the server is fully operational. The order of on_start() calls determines which actions are available when each plugin starts:
| Plugin | Registers During on_start() | Depends On |
|---|---|---|
transport.rest | HTTP listener, route table | None |
store.sqlite or store.postgres | Store actions | None |
handler.authd | 21 auth actions, 21 routes | Store |
handler.adl | Domain management + per-domain CRUD actions | Store, authd (for permission resolution) |
handler.seeder | Seed execution (if run_on_start: true) | ADL (tables must exist), authd (roles/users) |
scripting.lua | Lua script handlers, bus subscriptions | ADL (hooks reference domain actions) |
The seeder MUST be the last handler plugin in the configuration. It dispatches to ADL and authd actions that must already be registered.
on_stop()
The plugin stops accepting new work. The transport plugin closes its listener socket. Store plugins flush pending writes. The plugin SHOULD complete any in-flight requests before returning.
on_destroy()
The plugin releases all resources — database connections, file handles, memory allocations, thread pools. After on_destroy() returns, the plugin’s shared library MAY be unloaded.
2.4 Four Plugin Archetypes
Plugins are classified into four archetypes based on their role in the request lifecycle. The archetype is declared by the plugin and recorded in the plugin registry. The kernel does not enforce archetype-specific behavior — the classification is for documentation and introspection.
Transport
A transport plugin exposes the message bus to external clients. It translates protocol-specific requests (HTTP, WebSocket, gRPC) into Request objects, dispatches them through ICore::handle_request(), and translates the Response back to the protocol format.
The reference transport is kalos_rest, which implements:
- HTTP/1.1 server (via cpp-httplib)
- TLS termination (via OpenSSL 3.x)
- CORS handling
- Rate limiting
- Middleware chain (§4.2)
- Route-to-action mapping
A transport plugin MUST set the transport field on every Request it dispatches. This field is used by the middleware chain to distinguish external requests (which must pass through authentication and authorization) from internal requests (which may bypass middleware).
Store
A store plugin persists and retrieves data. It implements a query interface that the ADL engine’s action handlers call to execute CRUD operations, run migrations, and manage schema state.
Reference implementations:
kalos_sqlite— SQLite 3.x with WAL mode, connection pooling, and PRAGMA configurationkalos_postgres— PostgreSQL via libpq with connection pooling and SSL support
A store plugin MUST support transactional writes. A store plugin SHOULD support concurrent reads during a write transaction.
Handler
A handler plugin registers actions on the message bus and processes requests. All business logic lives in handler plugins.
Reference implementations:
kalos_adl— The ADL engine. Parses domain YAML, generates schema, registers CRUD actions, manages migrations.kalos_authd— Authentication and authorization. JWT token lifecycle, user management, role management, RBAC enforcement.kalos_seeder— Data seeding. Reads a YAML manifest and dispatches create requests for roles, users, and domain data.
Scripting
A scripting plugin extends handler behavior at runtime by executing user-provided scripts. Scripts are loaded from the filesystem and may be hot-reloaded without restarting the server.
The reference implementation is kalos_lua, which embeds Lua 5.4 via sol2 and provides:
- Hook execution (before/after create, update, delete, validate)
- Bus bridge (
kalos.call()for cross-action invocation) - Store access (direct database queries)
- Sandboxed execution (instruction limits, restricted
require) - Hot-reload (file watcher triggers re-registration)
2.5 Plugin Loading
Plugins are loaded from shared libraries (.so on Linux, .dll on Windows, .dylib on macOS) specified in the configuration file.
{
"plugins": [
{
"name": "transport.rest",
"path": "./plugins/kalos_rest.so",
"config": { ... }
},
{
"name": "store.sqlite",
"path": "./plugins/kalos_sqlite.so",
"config": { ... }
}
]
}
For each entry in the plugins array, the kernel:
- Calls
dlopen(path)(orLoadLibrary(path)) to load the shared library. - Resolves the
create_pluginfactory function exported by the library. - Calls
create_plugin()to obtain anIPlugin*instance. - Records the plugin in the registry under the given
name. - Proceeds to the lifecycle phases (§2.3).
If dlopen fails (library not found, symbol resolution error), the kernel MUST log the error and terminate. A missing plugin is a fatal configuration error.
Plugin configuration is passed to the plugin during on_register() as a subsection of the global configuration tree, accessible via ICore::config(). Each plugin reads its own configuration keys and ignores keys belonging to other plugins.
2.6 Message Bus
The message bus is the kernel’s central dispatch mechanism. All inter-plugin communication flows through the bus.
Action Registration
A plugin registers an action by calling ICore::register_action(name, handler), where name is a string identifier and handler is a callable that accepts a Request and returns a Response.
Action names follow the convention {domain}.{resource}.{operation} for ADL-generated actions:
adl.blog.post.list
adl.blog.post.get
adl.blog.post.create
adl.blog.post.update
adl.blog.post.delete
adl.blog.post.publish (custom operation)
adl.blog.post.assign (state machine transition)
System actions follow the convention {plugin}.{entity}.{operation}:
auth.login
auth.refresh
admin.users.list
admin.roles.create
adl.domain.submit
adl.domain.status
seeder.run
Action Dispatch
When ICore::handle_request(request) is called:
- The kernel looks up
request.actionin the action registry. - If no handler is registered, the kernel returns a
Responsewith code 404 and message “Action not found.” - If a handler is registered, the kernel invokes it with the
Requestand returns theResponse.
Dispatch is synchronous. The calling thread blocks until the handler returns. There is no action queue, no async dispatch, and no retry mechanism at the bus level. Asynchronous behavior (webhook delivery, notification sending, workflow advancement) is implemented by the responsible plugin using its own threading model.
Event Emission
In addition to action dispatch, the bus supports publish-subscribe events. A plugin calls ICore::emit(event_name, data) to broadcast an event. Other plugins that have called ICore::subscribe(event_name, callback) receive the event asynchronously.
Events are used for:
- History event recording (the ADL engine emits events; the history writer subscribes)
- Notification triggering (the history writer emits events; the notification engine subscribes)
- Workflow advancement (resources emit transition events; the workflow engine subscribes)
- Lua hook execution (the ADL engine emits lifecycle events; the Lua plugin subscribes)
- Integration triggers (history events trigger outbound API calls)
Event names follow the convention adl.{domain}.{resource}.{event}:
adl.blog.post.created
adl.blog.post.updated
adl.blog.post.deleted
adl.blog.post.transition.publish
adl.blog.post.history.articlePublished
Event delivery is fire-and-forget. The emitter does not wait for subscribers to process the event. If a subscriber throws an exception, the kernel logs the error and continues delivery to remaining subscribers. Event delivery order across subscribers is undefined.
2.7 Management Socket
The kernel opens a management socket at startup for out-of-band administration. The socket path is configured via core.management_socket in the configuration file.
On POSIX systems, the management socket is a Unix domain socket. On Windows, it is a named pipe.
The management socket accepts line-delimited JSON commands and returns JSON responses. The following commands are defined:
| Command | Response | Description |
|---|---|---|
{"cmd": "status"} | {"status": "running", "uptime": 3600, "plugins": [...]} | Server status and uptime |
{"cmd": "plugins"} | {"plugins": [{"name": "...", "archetype": "...", "version": "..."}]} | Loaded plugin inventory |
{"cmd": "actions"} | {"actions": ["adl.blog.post.list", ...]} | Registered action inventory |
{"cmd": "shutdown"} | {"status": "shutting_down"} | Initiate graceful shutdown |
The management socket MUST NOT be accessible from the network. It is a local-only administration interface. Authentication is provided by filesystem permissions on the socket file (POSIX) or named pipe ACLs (Windows).
2.8 Known Limitations
Two kernel-level limitations exist in the current design. Both are documented here to inform domain authors and plugin developers of their implications.
No Action Unregistration
ICore provides register_action() but no unregister_action(). Once an action is registered, it remains in the registry for the lifetime of the process. This has two consequences:
-
Hot-reload of Lua scripts that change their action registrations may leave stale entries. The Lua plugin mitigates this by letting stale handlers return HTTP 500 with a descriptive error message.
-
Domain unloading is not possible without restarting the server. A domain that has been submitted and activated cannot be removed — only replaced by submitting a new version.
This limitation is acceptable for single-domain deployments and for development workflows where server restarts are inexpensive. It becomes a constraint for multi-domain hosting scenarios where domains must be independently loaded and unloaded.
Convention-Enforced System Identity
Internal requests (from the seeder, Lua scripts, workflow steps, and scheduled tasks) carry the __system__ identity, which bypasses RBAC checks. This identity is convention-enforced: any plugin that constructs a Request can set the identity to __system__. The kernel does not verify that the caller is authorized to use the system identity.
This is acceptable for a single-operator platform where all plugins are trusted. It is a security vulnerability in any scenario where untrusted plugins are loaded or where multiple tenants share a server instance. A future version of the kernel SHOULD enforce system identity through a capability-based mechanism rather than a string convention.
End of Chapter 2.
Next: Chapter 3 — Configuration