Skip to content

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:

  1. Where the line falls between what stays compiled into the kernel and what moves to userland. Today open_ticket/raise_alarm logic is Rust in kernel-core (ticket.rs, fault.rs) — the leak 0031 condemned — while create_entity/install_schema are also Rust in kernel-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.

  2. 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-entity touches 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 the system package’s built-ins. They are the checkpoints — authorization (ADR-0021 acquire) 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-schema is 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 recipe interpreter + actions in the schema document (normal recipes); then the Function runtime — a sandboxed TS/Python host with capability-injected access to privileged Actions via sdkgen-generated bindings; then the Process substrate for long-running/triggered work.
  • sdkgen is 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.