Kernel Runtime
The KernelRuntime is the central component of LumenFlow. Every executable tool call passes through a host-authenticated kernel boundary before anything executes. Transport input is never execution authority.
The kernel is domain-agnostic. It knows portable workspace, lane, task, run, scope, policy, and capability primitives, but it does not know Software Delivery Work Units or gates. Those concepts come from packs. The kernel provides four capabilities that any pack can build on:
- Scope intersection — permission enforcement
- Policy evaluation — deny-wins rule cascade
- Evidence recording — immutable audit trail
- Tool dispatch — routing calls to pack-provided handlers
What Happens When a Tool Is Called
Section titled “What Happens When a Tool Is Called”Before this pipeline starts, the host authenticates an opaque caller credential. The kernel returns
a sealed AuthenticatedExecutionRuntime whose caller binding cannot be serialized, copied into a
request, or reconstructed from its public fields. Every call then supplies an
ExecutionSelector containing exactly task_id, run_id, and session_id. Those three values
select persisted state; they do not grant authority.
When the authenticated facade calls a tool (for example, fs:write), the kernel executes a strict
pipeline:
-
Persisted authority projection. The kernel resolves the exact selector against the immutable task spec and current event projection. The task must be active, the run must be executing, and the run session, principal, workspace, and lane must match the authenticated caller and loaded workspace. A selector cannot override any of these values.
-
Tool lookup. The kernel checks the tool registry. If the tool name is not registered by any loaded pack, the call fails with
TOOL_NOT_FOUND. -
Scope and profile resolution. The kernel projects the pinned workspace, lane, immutable task, loaded capability, pack integrity, and persisted execution-policy profile. It computes the scope intersection. Empty intersections and missing or unknown required profiles fail closed.
-
Reserved path check. Attempts to write kernel authority or credential paths under
.lumenflow/**or the workspace.envare denied independently of task input. -
Input persistence. The tool’s input is serialized, hashed with SHA-256, and written to the evidence store as a content-addressed blob. This happens before execution — even if the tool fails, the input is recorded.
-
Policy evaluation. The kernel’s policy engine evaluates all matching rules with the
on_tool_requesttrigger. A single deny from any layer makes the decision final.approval_requiredfails closed before the tool handler runs and returnsAPPROVAL_REQUIREDwith a request identifier and the matching policy decisions. -
Input validation and dispatch. The kernel validates the declared input schema, then routes the call to an authority-aware in-process intrinsic or a physically confined subprocess. For the exact-file mount and brokered-write boundary, see Tool Execution.
-
Evidence recording. The immutable authority fingerprint, input, result, scope intersection, policy decisions, pack integrity, and timing are appended to the evidence store. Success, failure, denial, cancellation, and crash all remain attributable to the exact persisted execution.
Steps 1–6 are the authorization gate. If any check fails, the tool never executes. Transport payloads cannot add workspace, lane, task, policy-profile, capability, or caller authority.
executeTool/executeApprovedTool return only the tool’s plain output. Callers that need the exact
receipt_id this evidence was recorded under — for example a durable bridge that must correlate a
receipt with a later terminal transition — should call the sibling
executeToolWithReceipt/executeApprovedToolWithReceipt (WU-3409) instead: same authorization
pipeline and dispatch, but the typed result is { output, receipt_id }, with receipt_id set to the
exact receipt this call’s evidence was appended under (null only when no evidence was ever created,
e.g. a TOOL_NOT_FOUND early-out before capability resolution). Callers never need to scan the
evidence store before and after a call to discover which trace is theirs. These two methods are
additive and optional on AuthenticatedExecutionRuntime — every runtime bound through
bindAuthenticatedExecutionRuntime provides them, but the interface does not require them, so the
pre-existing plain-ToolOutput methods and their many callers (MCP server, HTTP tool-api, CLI
inspect/task-claim/heartbeat, packs/agent-runtime) are unaffected.
Initialization
Section titled “Initialization”The kernel is created with a workspace root and an explicit event-log factory. A runtime without a
host caller authenticator can manage lifecycle state, but executable calls fail with the typed
CALLER_UNBOUND error.
Local Node.js surfaces should use the host-owned binder. It validates every selector against the persisted active task and run, derives the principal and workspace from that projection, and keeps the one-use credential private inside the host closure:
The caller owns the returned host and must await host.close() during shutdown. Closing stops
new work, drains operations that already started, runs ToolHost shutdown reconciliation, and then
closes the event log. Repeated close calls are safe.
The ExecutionSelector schema is strict: extra caller, workspace, lane, scope, pack, profile, or
capability fields are rejected. A local host binds one batch only, and every selector in that batch
must resolve to the same persisted principal and workspace before the first handler can run.
Canonical Identifiers
Section titled “Canonical Identifiers”Workspace, task, run, session, receipt, and event identifiers are selectors and persisted names, not arbitrary strings. Their canonical grammar is:
- the first character is a Unicode letter or number
- later characters are Unicode letters or numbers,
.,_,:, or- - the entire value is NFC-normalized
/,\, percent-encoded separators, absolute paths,.and.., null bytes, control characters, leading/trailing whitespace, and other aliases are rejected
Validation never slugifies, decodes, trims, or normalizes an invalid value in place. A
NON_CANONICAL_IDENTIFIER failure includes the NFC migration target, while
INVALID_CANONICAL_IDENTIFIER explains which invariant failed. Operators must choose the new
identifier and migrate every persisted reference explicitly.
Existing identifiers that already satisfy the grammar remain valid without modification. For a legacy alias, stop every writer, inventory task specs, event payloads, evidence receipts, session records, and external references, rewrite them as one migration, then restart and replay validation. Do not create a compatibility map that accepts both aliases indefinitely.
Protected State Port
Section titled “Protected State Port”The kernel expresses task and evidence locations as ProtectedStatePath values and depends on the
backend-neutral ProtectedStatePort. The port covers directory creation, read and range-read,
exclusive write, immutable publication, append, atomic replacement, rename, listing, metadata, and
removal. It contains no host root and exposes no realpath, file descriptor, or
directory-creation primitive to kernel policy code.
createLocalEventLogFactory({ workspaceRoot }) attaches the Linux host binding automatically.
Task specs and evidence then use descriptor-held, no-follow operations for their complete lifetime.
Missing directories are created relative to the nearest held existing ancestor; replacing the
visible parent name with a symlink cannot redirect the pending leaf operation.
Custom event-log factories must attach an equivalent binding. Initialization fails closed when it
is absent, and caller-supplied taskSpecRoot or evidenceRoot overrides are rejected instead of
silently bypassing or replacing the binding. Explicitly ephemeral hosts and tests can attach
InMemoryProtectedStateAdapter.
Remote Authentication Boundary
Section titled “Remote Authentication Boundary”HTTP and MCP Streamable HTTP use the same canonical signed remote credential model. The host accepts a credential only when all of these checks pass:
- the signature and expiry are valid
- the issuer equals the configured issuer
- the audience equals the exact configured resource URL
- the workspace equals the loaded workspace
- the subject is present and canonical
- every transport permission is from the supported permission vocabulary
The permissions are task:read, task:write, event:read,
evidence:read, discovery:read, tool:invoke, and ag-ui:run. They are
transport permissions, not kernel tool scopes. Legacy scopes such as tool:*
cannot authorize an HTTP route.
Authentication happens before URL parsing, permission routing, request-body
reads, MCP session lookup, or runtime state access. Invalid credentials produce
401 invalid_token without revealing which credential check failed. A valid
credential without the required permission produces 403 insufficient_scope.
The default HTTP authenticator denies every request; opaque bearer fallback is
not enabled unless a host deliberately supplies a different adapter.
After verification, the bearer is discarded. The HTTP host retains a sanitized
principal, workspace, resource, and permission list, and creates a one-request
opaque kernel credential backed by a private WeakMap. Only that opaque object
can bind the authenticated lifecycle or execution facade. It is not
serializable, and the bearer itself never reaches the kernel, handlers,
evidence, logs, or persisted session state.
Remote request bodies are data, not authority. Lifecycle routes reject caller-authored task IDs, workspace IDs, principals, agent IDs, claim identities, scopes, session IDs, and request IDs. The server generates remote task IDs, persists the authenticated principal as owner, and generates claim session and run IDs. Inspection, claim, completion, event streaming, and evidence access must continue to resolve to that same owner.
The web runtime reads the workspace ID from workspace.yaml and requires
LUMENFLOW_WEB_HTTP_ISSUER, LUMENFLOW_WEB_HTTP_RESOURCE, and
LUMENFLOW_CONTROL_PLANE_SIGNING_KEY. The issuer and resource must be absolute
URLs, and the resource is also the credential’s exact audience.
MCP Streamable HTTP exposes only its RFC 9728 protected-resource metadata
without authentication. Every protocol request is authenticated first, and
each Mcp-Session-Id is bound to the exact sanitized principal, workspace,
resource, and permission set established at initialization. Changing any part
of that context is rejected. Successful transport admission still passes
through kernel ownership, scope, policy, approval, and evidence checks. MCP
stdio remains a local process-environment credential boundary and does not run
the HTTP bearer flow.
The web production launcher enables Next’s manual signal handling in a shell-independent Node
wrapper. On POSIX systems, the production smoke sends a real SIGTERM and requires the runtime
closed marker before the server exits. Node’s Windows subprocess API terminates a child for these
signals without delivering its JavaScript listener, so the Windows build smoke still validates the
native runtime, replay, and SQLite database while the platform-neutral shutdown-controller test
drives the listener directly and proves close-once behavior.
The explicit binding keeps state-path authority in @lumenflow/host: the kernel owns identifier
validation, path intent, task/evidence behavior, and ports, while the host owns protected
filesystem access, SQLite, WAL, filesystem locking, and legacy migration. Workspace and pack
discovery I/O remains an initialization adapter concern being separated further by INIT-087.
During initialization:
- The
workspace.yamlspec is read and hashed (SHA-256). This hash is checked on every subsequentexecuteToolcall — if the spec changes while the kernel is running, all tool calls returnSPEC_TAMPEREDuntil the kernel is restarted. - All packs referenced in the workspace spec are loaded, validated, and their tools registered.
- Pack-specific workspace config is validated against each pack’s declared
config_schemaand made available to runtime capability and policy factories. - The policy engine is constructed with a four-layer stack built from pack-declared policies plus any runtime-authored rules returned by pack
policy_factoryhooks. - The injected event-log adapter and evidence store are initialized; any orphaned evidence traces from a previous crash are reconciled.
Local Daemon Delivery Safety
Section titled “Local Daemon Delivery Safety”The optional local runtime daemon uses strict newline-delimited protocol v1 frames. A request has a
per-attempt correlation_id and a caller-stable idempotency_key. The daemon owns method mutation
classification—scheduler.claim-next is mutating even if caller input claims otherwise—and the
strict request schema rejects caller-supplied classification fields.
The client permits the in-process command path only when the socket connection fails before its
first write attempt. Immediately before calling socket.write, it records that delivery is
possible. A later timeout, disconnect, socket error, malformed or oversized response, invalid
response schema, or correlation mismatch returns a typed status: 'ambiguous_delivery' result and
does not call the local handler.
Request and response frames are limited to 64 KiB including the newline delimiter. Both sides validate protocol version, strict schema, JSON/UTF-8 framing, and response identity. Matching well-formed daemon success and failure responses remain terminal responses and do not invoke fallback.
Daemon-outcome ledger
Section titled “Daemon-outcome ledger”Reconciling an ambiguous delivery, or retrying any mutating command, resolves against the
daemon-outcome ledger: the same workspace-scoped RunAggregate compare-and-swap authority the
scheduler and session use, not a separate idempotency database or an in-process cache. Before
running a handler, the daemon hashes the trusted workspace scope, the caller’s idempotency_key,
and a canonical fingerprint of protocol version, method, and validated parameters, then wins a
lease reservation for that key. The fencing key is a per-attempt token generated fresh for every
call, not the daemon instance’s owner_id — owner_id identifies the process for cross-instance
rejection and diagnostics only, and is not attempt-unique. Only the attempt that actually wins the
reservation runs the handler; its bounded, JSON-safe, versioned result commits back onto that exact
reservation, proven by the same attempt token, as the terminal, replayable outcome. A second,
genuinely concurrent in-flight request from the same daemon process never executes a second time
— it waits for the winning attempt’s committed outcome instead of racing it.
This distinguishes four things operators otherwise conflate:
- Transport ambiguity — the client does not know whether a request reached the daemon.
- Retry identity — the
idempotency_keysays which attempt this is; alone it proves nothing about execution, and it is not attempt-unique across concurrent requests from one process. - Committed aggregate outcome — the ledger entry is the one place proving whether a mutation ran and what it produced. The same key and fingerprint always replay that committed outcome; the same key with a different fingerprint fails closed instead of executing.
- Operator recovery — a crash before the reservation commits is safe to retry immediately; a crash after commit but before the reply reaches the caller replays the stored outcome; a same-process concurrent duplicate waits for the winner; a reservation held by a still-live different instance rejects a competing attempt until its lease expires, so recovery is: wait out the lease (or restart the stuck instance), then resend the exact original request.
Recoverable scheduler ownership
Section titled “Recoverable scheduler ownership”The scheduler uses one committed scheduling contention stream as authority. Enqueue resolves
priority and lane from the authoritative task specification, rejects caller promotion or unknown
lanes, and persists the positive WIP and lease policy. Atomic scheduler.claim-next commits owner,
attempt, lease expiry, and a monotonic fencing token before returning work, removing both the
cross-task WIP race and the dequeue-then-start crash window. Competing runtimes replay after
expected-version conflicts before selecting again.
Heartbeat, completion, release, cancellation, and takeover require the exact current token. A stale
generation fails closed. Expiry uses the trusted runtime clock, and takeover records a durable
expired or released reason. Restarted daemons rebuild queue and ownership projections from
event history; process memory is only a cache.
Task Lifecycle
Section titled “Task Lifecycle”The kernel manages tasks through an event-sourced state machine:
| Method | Transition | Policy trigger |
| -------------- | ----------------------- | --------------- |
| createTask | — (writes spec to disk) | None |
| claimTask | ready → active | on_claim |
| blockTask | active → blocked | None |
| unblockTask | blocked → active | None |
| completeTask | active → done | on_completion |
| failTask | active → done | None |
| cancelTask | active → done | None |
failTask and cancelTask (WU-3409) are terminal — unlike blockTask, which only pauses a
resumable run, they reach the aggregate’s failed/cancelled status and are absorbing (no further
lifecycle command may mutate the task). They exist because before WU-3409, KernelRuntime exposed no
way to reach RunAggregate’s pre-existing fail/cancel commands at all — only succeed (via
completeTask) was wired. Both accept an optional connected_compute payload that batches a
canonical receipt and cloud terminal-outbox intent into the same atomic append as the terminal event;
see connected-compute-local-runner.md “Atomic Receipt And Terminal-Outbox Batch (WU-3409)”.
Task specs are immutable YAML files stored at .lumenflow/kernel/tasks/<id>.yaml. State is projected from an append-only event log — there is no mutable state file.
Task creation stages the complete spec under a temporary protected-state name, flushes it, and
atomically links it to the final name without replacement before flushing the parent directory and
appending task_created. Readers therefore see either no spec or the complete immutable spec,
never a partial file. An identical retry may verify the existing immutable spec against the one
creation event, but never appends a second creation event. Reads reject symlinks, non-regular
entries, hard links, identity mismatches, replaced ancestors, and non-canonical identifiers. The
Linux host keeps the selected ancestor descriptor open across the leaf operation; a pre-open
realpath check is diagnostic only and is never the confinement boundary.
When task-spec ancestors are created, the Linux host flushes each held parent descriptor in root-to-leaf order before immutable publication can return. A retry after an interrupted directory sync re-establishes the complete ancestor durability chain, including entries that the earlier attempt created but did not durably flush.
New task_created events mark spec_hash_algorithm: canonical-json-v2. Version 2 sorts object keys
with locale-independent UTF-16 ordering and accepts only plain JSON values. It rejects non-finite
numbers and non-JSON object types such as maps, sets, dates, and class instances before publication,
so moving a workspace between host locales cannot change an untampered task’s identity and YAML
.nan/.inf values cannot collide with JSON null. Unmarked events remain a legacy compatibility
boundary: the runtime verifies their stored hash against the runtime’s supported two- and
three-letter ICU language collations plus the historical script variants, while marked version-2
events accept only the version-2 hash.
Run Identity Is Pinned At Task Creation
Section titled “Run Identity Is Pinned At Task Creation”claimTask accepts a caller-supplied session_id, but that value is not
what gets persisted on the run. Each task’s RunAggregate identity
(run_id, session_id, workflow_id) is derived once, at task-creation
time, from TaskSpec.extensions (extensions.run_id, extensions.session_id,
extensions.workflow_id), defaulting to session-${task.id} /
workflow-${task.id} when the extension is absent. Every subsequent
claimTask, blockTask, unblockTask, and completeTask call for that task
reuses this pinned identity — the session_id argument in ClaimTaskInput
only participates in policy/approval evaluation, never in the persisted run
identity.
Consumers that need claimTask’s returned run.session_id to equal a
specific caller session — for example a durable execution bridge that later
compares its own session id against the persisted run for ownership checks —
must declare that session id as extensions.session_id on the TaskSpec
passed to createTask, before the first claim. WU-3409 (@lumenflow/cli
Connected Compute execution binding) is the reference consumer: see
connected-compute-local-runner.md “Local Execution Activation (WU-3409)”
for the concrete defect this pinning behavior caused when left undeclared.
The identity tuple (run_id/session_id/workflow_id) stays pinned to the
first claimant forever by design — no lifecycle command mutates it, and
evolveRunAggregate throws RunAggregateIntegrityError if history ever
tries. A different-session restart on an already-active (non-terminal) task
is not, therefore, resolved by changing the identity. Instead
claimTask/reclaimConnectedComputeRun (WU-3409) also track a separate,
mutable connected_compute_fencing_generation per task, set at claim time
from an optional connected_compute_fencing_generation input and updated by
a new fenced_reclaim RunAggregate command: a caller presenting a strictly
greater generation than the one currently recorded may re-fence the task’s
owner_id/generation/attempt (the mutable claim fence, not the immutable
identity) to itself. A same-or-lower generation fails closed with
RunAggregateCommandError. This is how a genuine cloud-fenced takeover
(WU-3408’s fencing_generation) recovers a crashed session’s run without
ever weakening the identity-immutability invariant above; see
connected-compute-local-runner.md “Atomic Receipt And Terminal-Outbox
Batch (WU-3409)” for the full reconciliation flow, including why the
execution binding’s own TaskSpec-equality check (a separate, binding-local
guard unrelated to RunAggregate identity) also had to stop comparing
extensions.session_id for this to work end-to-end.
Event authority and concurrency
Section titled “Event authority and concurrency”EventLogPort is the backend-neutral authority. Adapters assign a monotonic sequence, an opaque
cursor, and a per-stream version to every accepted event. Timestamps remain event data and never
decide persistence order. replay() pages in sequence order, while readStream() returns the full
consistent stream snapshot used for lifecycle projection, including streams longer than 100
events.
Durable event payloads accept only lossless JSON values. In particular, domain_data rejects dates,
maps, sets, undefined values, non-finite numbers, big integers, and class instances before an
adapter append, so the append result and a later SQLite replay cannot disagree.
Task lifecycle methods read a stream version and append with expectedStreamVersion. If another
writer wins first, the append fails atomically with EventLogConflictError and code
EVENT_LOG_CONFLICT; no part of a multi-event transition is persisted. Callers should re-read the
stream before deciding whether to retry.
The local host adapter uses SQLite transactions with WAL journaling. On first open it takes the
existing JSONL writer lock, snapshots and validates the legacy file, commits events plus a source
fingerprint marker, and archives the source as events.jsonl.migrated before releasing the lock.
Interrupted migration is restartable: pre-commit work rolls back, while a committed marker resumes
the archive step without importing records twice. Re-creating the old JSONL source after migration
is treated as drift and fails closed.
An existing legacy lock also fails closed after the configured acquisition retries. The adapter
does not remove locks automatically because an older JSONL writer cannot participate in SQLite
fencing. New locks use a v2 shape with owner_id and owner_pid but omit the numeric pid required
by the frozen JSONL parser. Old writers therefore treat a live v2 lock as opaque and cannot
auto-delete it, including across PID namespaces. To recover, stop and establish operational
quiescence for every process or container using the workspace, including legacy JSONL writers and
current SQLite adapters. A valid v2 owner_pid is supporting evidence only; it cannot prove
quiescence across containers or PID namespaces. Only then remove the exact stranded lock, restart
the adapter, and retry. Never delete a lock while any legacy or current writer may be active.
createLocalEventLogFactory({ workspaceRoot }) uses the canonical legacy source and lock under
.lumenflow/kernel/events/. Explicit host-factory paths take precedence over deprecated runtime
path hints; those runtime hints remain compatibility inputs only and should not be used in new
integrations.
Approval-required transitions
Section titled “Approval-required transitions”approval_required is a non-executable policy result for tools, claims, and completions:
claimTaskandcompleteTaskappendapproval_requested, then throw a typedApprovalRequiredError. They append no protected lifecycle transition.executeToolreturnsAPPROVAL_REQUIRED, persists the denied receipt and exact request, and does not call the handler.- the request binds task, run, session, principal, lane, final enforced scopes, deny overlays, tool and pack identity, exact input digest, stable capability descriptor, policy, configuration, runtime version, original denied receipt, and finite expiry.
- a host first authenticates a reviewer with
authenticateApprovalCaller(). A separate host-owned authorizer must permit that reviewer for the request. Resolution input contains onlyrequest_id,task_id,decision, andreason; the kernel derives the resolver identity from the authenticated closure. - resolution never emits
task_resumedortask_blockedand never executes an effect. Approval state is projected separately from task lifecycle state. - retry revalidates the current trusted authority, policy, configuration, input, scopes, runtime, workspace integrity, and expiry. Any drift fails closed.
- one compare-and-swap append consumes the grant before a tool attempt. Claim and completion append consumption, lifecycle transition, and terminal effect atomically.
Host-Driven Agent Loops
Section titled “Host-Driven Agent Loops”Agent orchestration can stay above the kernel while still using kernel-enforced governance. A host
uses one sealed AuthenticatedExecutionRuntime and one exact selector to:
- call
agent-runtime:execute-turn - inspect the governed turn output for
status: tool_request - verify the tool is present in the exact host-provided catalog
- call the requested tool through
executeTool()with the same selector - feed the tool result back into the next
agent-runtime:execute-turn
This keeps the model-turn loop in host code, but the actual tool request still passes through scope enforcement, the task’s persisted policy profile, approvals, and evidence recording. The orchestration helper counts turns and tool calls against the configured ceilings without putting authority metadata into the selector.
Connected Compute Runner Nodes
Section titled “Connected Compute Runner Nodes”WU-3361 composed and tested the same kernel boundary for operator-managed Connected Compute nodes; WU-3409 subsequently wired the durable execution bridge into the production compute-run entrypoint. The runner refuses execution-bearing work before runner registration, claim, provider invocation, tool discovery/execution, sandbox dispatch, evidence delivery, or terminal side effects.
The runner is pull-only. It opens no inbound listener, exposes no remote shell, and does not fall
back to a hosted model. WU-3408 bridges the cloud lease to a monotonic ownership generation
(fencing_generation, distinct from the opaque lease_token), end-to-end cancellation, trusted
configuration/network authority, and the host catalog needed to bind a real assignment to its
persisted WU, workspace, lane, checkout, and run.
Every claim response carries a positive, strictly increasing fencing_generation. The runner
captures it once at claim time and carries the exact same value on every subsequent lifecycle call
— ack, extend, complete, fail, release, heartbeat, and tool-dispatch evidence ingestion. The control
plane is the compare-and-swap authority: a call carrying a superseded generation is rejected with
fencing_generation_stale even when lease_token still matches, so a takeover after crash, an
expired lease, or a failed renewal cannot admit split-brain execution — the superseded owner’s calls
are rejected atomically rather than silently accepted. The client never derives a generation from
assignment_id or lease_token; a claim response that omits or corrupts it fails closed (surfaced
by lumenflow compute doctor’s fencingCapability check), mirroring the local scheduler fencing
token described above but scoped to the cloud lease/runner boundary rather than the in-process
scheduler.
WU-3409 adds durable canonical-receipt and terminal outboxes, restart reconciliation, and a
separate sealed host lifecycle gate (see “Terminal Lifecycle Commands” below and
connected-compute-local-runner.md “Atomic Receipt And Terminal-Outbox Batch” and “Sealed Host
Lifecycle Adapter”). A completed code assignment’s gate check runs through that sealed adapter —
never through the model’s own governed tool authority — and every terminal delivery (complete, fail,
release/cancel) is sealed into the same atomic RunAggregate append as the canonical receipt, then
drained to the cloud idempotently under the exact fencing generation the runner holds. Configuration
and assignment payloads still cannot activate or broaden the composed WU-3361 boundary.
Installable runner packages are advertised by release channel. Private test artifacts and internal
release candidates may use one-time bootstrap paths, but public npm, WinGet, Homebrew, MSI, .pkg,
.deb, .rpm, or tarball instructions require artifact manifest evidence plus installation,
doctor, and service smoke evidence for that channel.
Runtime-Visible Pack Config
Section titled “Runtime-Visible Pack Config”Packs can declare a config_key and config_schema in their manifest. During runtime startup, the
kernel validates the workspace config payload for that pack and threads the resolved value into:
- capability factories — for config-aware
required_scopesandrequired_env - policy factories — for runtime-authored conditional rules
This is how the agent-runtime pack derives provider host allowlists, validates credential env
references, and turns persisted execution-policy profiles into kernel-enforced decisions.
Policy-Aware Tool Discovery
Section titled “Policy-Aware Tool Discovery”Hosts do not need to reconstruct tool gating in application code. They ask the authenticated execution facade for the already-filtered tool set:
listGovernedTools() projects the same persisted authority and evaluates the same scope and policy
rules used during real execution. The result includes allow and approval_required entries but
excludes denied and out-of-scope tools. The composed Connected Compute bridge also applies an
explicit host allowlist, but production does not expose that catalog until WU-3408 installs the
fenced bridge.
Pack-Owned Agent-Session Orchestration
Section titled “Pack-Owned Agent-Session Orchestration”Framework orchestration can remain above the kernel, but packs can still own their domain runtime
state. The agent-runtime pack uses this boundary to persist:
- linear
agent-sessionresume state - workflow branch and join readiness
- scheduled wakeups
- continuation records explaining why a session resumed
Those orchestration records live in pack state under .agent-runtime/workflow/, while the kernel
continues to own approval state, tool execution, and immutable evidence receipts.
Filesystem Layout
Section titled “Filesystem Layout”The kernel stores all state under .lumenflow/kernel/ in the workspace root:
Event records are immutable at the logical contract even though SQLite manages database pages and
WAL checkpoints internally. Task specs, migrated JSONL archives, evidence traces, and input blobs
remain write-once or append-only. Long-lived hosts that directly own an event-log port must await
close() to stop and drain subscription polling; repeated close calls are safe.
An EventSubscription reports running, degraded, or terminal health, its last successful
cursor, and its last typed error. Callback or transient read failure does not advance the cursor.
Transient reads retry; corruption and legacy-source drift terminate visibly. After repair,
resume() continues from the last successful cursor.
.lumenflow/kernel/ is local runtime authority, not source. LumenFlow init and upgrade add a
canonical ignore rule for the workspace root and nested workspace paths in the same Git repository.
An independent nested Git repository must run init or upgrade itself because parent ignore rules do
not cross repository boundaries. The tree can contain sensitive task inputs and execution evidence,
so secure backups and restrict access accordingly.
Tool Error Codes
Section titled “Tool Error Codes”When a tool call fails at any stage in the pipeline, the kernel returns a structured error:
| Code | Stage | Meaning |
| ----------------------- | -------------- | ----------------------------------------------------------------- |
| TOOL_NOT_FOUND | Lookup | Tool name not registered by any pack |
| SCOPE_DENIED | Authorization | Scope intersection is empty or reserved path violation |
| POLICY_DENIED | Authorization | Policy engine returned a deny decision |
| APPROVAL_REQUIRED | Authorization | Policy blocked the action; no handler or lifecycle transition ran |
| SPEC_TAMPERED | Pre-check | Workspace spec changed since kernel startup |
| INVALID_INPUT | Validation | Input failed the tool’s schema |
| INVALID_OUTPUT | Post-execution | Output failed schema normalization |
| TOOL_EXECUTION_FAILED | Execution | Handler threw an exception |
Caller and selector failures throw typed ExecutionAuthorityError values before a handler can
run. Important codes include:
| Code | Meaning |
| ------------------------------ | ------------------------------------------------------------------------ |
| CALLER_UNBOUND | The host or transport has no authenticated execution caller |
| CALLER_AUTHENTICATION_FAILED | The opaque host credential was absent or rejected |
| SELECTOR_INVALID | The selector is malformed or contains fields beyond the strict three IDs |
| TASK_NOT_ACTIVE | The selected task is not active |
| RUN_NOT_EXECUTING | The selected run is absent or not executing |
| SESSION_MISMATCH | The selected session does not own the current persisted run |
| PRINCIPAL_MISMATCH | The authenticated caller does not match the persisted run principal |
| WORKSPACE_MISMATCH | The caller/task workspace does not match the loaded runtime |
| PROFILE_MISSING | Configured policy profiles require a persisted task selection |
| PROFILE_UNKNOWN | The persisted selection is absent from the pinned pack config |
| AUTHORITY_FORGED | A handler received an object not minted and sealed by this runtime |
Next Steps
Section titled “Next Steps”- Scope Intersection — How the 4-level permission model works
- Policy Engine — How deny-wins evaluation works
- Evidence Store — How the audit trail is built
- Packs — How tools are declared and loaded
- Create a Pack — Build your own domain pack