ADR-0035: Actions are privileged kernel primitives or normal userland programs
ADR-0035: Actions are privileged kernel primitives or normal userland programs
Status
Accepted. Concretizes and extends
ADR-0034 (domain operations are
reality-declared Acts) — it supplies the vocabulary and the language rule 0031
left open, and does not disturb its decision. Builds on
ADR-0033 (POST /sys/<verb>/<name>),
ADR-0026 (transitions are Acts),
ADR-0021 (one acquire() at the seam), and
ADR-0015 (declared
derivations — the seed of the Function layer).
Date
2026-07-05
Context
ADR-0031 ruled that domain operations (raise_alarm, open_ticket, …) are
reality-declared Acts dispatched through a generic Act verb, not bespoke kernel
methods, and named “a generic Act dispatcher + reality-declared action registry”
as the next kernel growth. That dispatcher now exists: an ActionRegistry
(name → handler, mirroring the worker’s JobHandler), a VinxiKernel::act(name, input) dispatcher, and a single POST /sys/act/{name} route. Every Act operation
is reached through one governed door.
Two things ADR-0031 left unspecified surfaced while wiring it up, and both are decisions, not implementation details:
-
Where the line falls between what stays compiled into the kernel and what moves to userland. Today
open_ticket/raise_alarmlogic is Rust inkernel-core(ticket.rs,fault.rs) — the leak 0031 condemned — whilecreate_entity/install_schemaare also Rust inkernel-core, and those are not a leak: they are the primitives. The registry holds both, so “reality-declared” alone does not say which is which. -
What language business logic is written in. If domain behaviour must be authored to extend the system, authoring it in Rust — recompiling the kernel to open a ticket differently — defeats the malleable-software goal. The kernel is meta-semantic precisely so the domains on it are software, not kernel forks.
The-seam frames the answer as rings and syscalls (§III–IV): a closed instruction set at ring-0, userland programs at ring-3, and “the SDK is the only door.” What was missing is the governed statement of that boundary for Actions.
Decision
Every Action is either privileged or normal. Privileged Actions are kernel
primitives — ring-0, implemented in Rust, the only code that touches a plane
directly. Normal Actions are userland programs that may only compose privileged
Actions, and are authored in a recipe (declarative JSON) or a function
(TypeScript/Python), never in Rust.
- The test is mechanical. Does the operation touch a plane/port directly
(write Truth, project World, record a Decision)? → privileged (Rust,
kernel-core). Does it only call other Actions? → normal (userland).open-ticket=record-decision+create-entity+assert-link; it composes only, so it is normal.create-entitytouches the World projection, so it is privileged. - Privileged Actions are the base instruction set:
create-entity,create-entities,install-schema,register-resource-version,record-decision, and the Assert-verb facts (assert-link,assert-series). They are registered at boot as thesystempackage’s built-ins. They are the checkpoints — authorization (ADR-0021acquire) is enforced here. - Normal Actions are declared in the ontology (alongside their kind) and bind
their behaviour to one of a ladder of escalating power, none of it Rust:
recipe— declarative composition of privileged Actions (mint/derive id → create/patch entity with declared traits → assert declared links → under a decision). Covers the create/patch-and-link majority (open-ticket,raise-alarm,transition,clear-alarm).function:<ref>— business logic in TypeScript/Python, sandboxed, reaching reality only by calling privileged Actions through the typed SDK. Covers what a recipe cannot express (confirm-root-cause’s RCA partition). This is the Function layer (its own follow-on decision); the derivation evaluator of ADR-0015 is its read-side seed.
- The language rule is a goal, not a tolerance. Rust is written only for the kernel machinery, the privileged Actions, and the Function host/sandbox. All business logic is TypeScript/Python (and JSON recipes). The kernel team touches Rust only to add a new primitive, gated by the-seam’s falsifiable test.
- Enforcement is capability + sandbox, not convention. A normal Action /
Function holds no ambient authority — it cannot reach a store; its only egress is
a call to a privileged Action, which crosses into ring-0 where it is authorized.
Privilege is structural (ring-0, touches a plane); capability is the runtime
gate — orthogonal axes (
install-schemais privileged and hard capability-gated).
This sits inside the three-layer ontology the-seam and Palantir’s Ontology both describe — semantic (kinds/traits/links), kinetic (Actions + Processes), logic (Functions) — with Action as the discrete kinetic member and Function as the shared logic layer that Actions and Processes invoke.
Alternatives Considered
- One undivided “Action” concept, migrate case by case without a rule: rejected — without the privileged/normal test, every migration re-litigates whether a given op belongs in the kernel; the boundary drifts and the leak reopens.
- Business logic in Rust (as today), authored via a plugin ABI: rejected — keeps domain behaviour a compile-time kernel concern; nobody extends the system without the Rust toolchain and a kernel rebuild, which is the malleability the meta-semantic kernel exists to avoid.
- A declarative DSL powerful enough for all behaviour (no code tier): rejected — a recipe grammar that grows conditionals, computed values, and graph walks becomes a scripting language inside the kernel; the bright line (recipe for composition, Function for computation) keeps the kernel small.
Consequences
- The privileged/normal line predicts what leaves
kernel-core. The normal ops’ Rust —ticket.rs,fault.rs— is deleted and re-declared as recipes / Functions; the privileged primitives (graph.rs,ontology.rs, the resource registry) stay. Of the nine Actions in the registry today, only the four-to-five privileged ones remain Rust after migration. - Build order (each a follow-on): the dispatch plane is done; next a
recipeinterpreter +actionsin the schema document (normal recipes); then the Function runtime — a sandboxed TS/Python host with capability-injected access to privileged Actions viasdkgen-generated bindings; then the Process substrate for long-running/triggered work. sdkgenis the ABI. The privileged Actions are the surface a Function is allowed to call; the typed SDK projected from the installed reality is how a Function names them, with capabilities lifted into the type signatures (the-seam §VIII).- A new privileged Action is the one reason to touch the core — it is a new primitive, and must pass the falsifiable test (express a real domain flow as the six verbs over the seven primitives). Everything else is software: recipes and Functions authored in userland, by domain teams and agents, without a kernel rebuild.