# Kernel header tiers — the public/internal API boundary **Status:** Design record. **Implemented** in the root tree: the four-tier header split below matches `kernel/tk_kernel.h` / `tk_kernel_port.h` / `tk_kernel_sync.h` / `tk_kernel_internal.h` almost verbatim (each header's banner states its own tier). Two items remain open, flagged inline: the per-symbol `[rt]`/`[setup]` privilege tags are *documented* but not yet *annotated* on individual declarations, and the introspection snapshot still leaks a `uint8_t` field. **Scope:** which kernel symbol lives in which header, and why. **Companions:** [composability.md](composability.md) (the layering law these tiers serve), [board-contract.md](board-contract.md) (who calls the Tier-B port contract), ADR [0001](../decisions/0001-foreign-architecture-policy.md) (the `tk_stack_cell_t` type Tier A exposes). --- ## 1. The problem this closes `tk_kernel.h` is the application header (every example includes it) — yet it also once declared **arch-port hooks** and **sync-construction primitives an app must never call**. The tell: `tk_kernel_wait()` documents "MUST be called holding the kernel lock (`tk_sched_enter`)", but `tk_sched_enter` isn't in the header, so a reader cannot honor the contract. The opaque handle redesign hid the TCB layout; these leaked entry points were the remnant. The tier split gives them their own headers. ## 2. The consumers (who includes what) | Consumer | Needs | Must NOT see | |---|---|---| | **Application** | thread lifecycle, sched config, sync objects, time | port hooks, wait-queue builders, TCB layout | | **Arch port** (`arch/`) | the kernel's tick/switch entry points + `kernel_arch.h` | app niceties, TCB layout | | **Sync/IPC builder** (kernel mutex/sem/cond **and** an app adding a new primitive) | wait queue + block/wake + current-thread | TCB layout, scheduler guts | | **Kernel internals** (`kernel/*.c`) | everything (TCB struct, kernel singleton, `tk_sched_enter/leave`) | — | | **Sovereign / HAL-only build** | nothing from the kernel | the whole kernel | ## 3. The four-tier boundary (the decision) | Tier | Header(s) at root | Prefix | Who includes it | |---|---|---|---| | **A — Application API** | `tk_kernel.h`, `tk_mutex.h`/`tk_sem.h`/`tk_cond.h`, `tk_time.h` (introspection under `TK_CONFIG_ENABLE_INTROSPECT`) | `tk_*` | the app (and examples) | | **B — Port contract** | `tk_kernel_port.h` | `tk_kernel_*` | `arch/` ports + the SVC/fault glue | | **C — IPC-extension contract** | `tk_kernel_sync.h` | `tk_kernel_*` | the kernel's sync + an app/lib building a new primitive | | **D — Kernel-internal** | `tk_kernel_internal.h` (+ `tk_sync_internal.h`) | `tk_*_internal`, statics | only `kernel/*.c` + unit tests | Naming: **`arch_*`** = what the port PROVIDES (`kernel_arch.h`, the arch contract); **`tk_kernel_*`** = what the kernel provides (to app *or* port — disambiguated by which header declares it); **`tk_*`** = app. **The tier is defined by the header that declares a symbol, not by the prefix.** > **Decided against — a distinct `tk_port_*` prefix for Tier B.** The analysis floated > renaming the ~7 Tier-B port hooks to `tk_port_*` so the tier is legible from the call > site. The root **kept the `tk_kernel_*` names** (`tk_kernel_tick_preempt`, > `tk_kernel_preempt_select`, …): the header that declares a symbol already names the > tier, and a second prefix convention would churn the arch ports for legibility the > `#include` line already provides. Rejected as cost without a guarantee. ## 4. The manifest — every symbol, placed **→ Tier A (`tk_kernel.h`, app-facing):** `tk_thread_create`/`tk_thread_spawn`/ `tk_thread_attr_*`, `tk_thread_exit`/`_join`/`_detach`/`_yield`/`_sleep_ms`, `tk_thread_set_quantum`/`_affinity`/`_pin`/`_privileged`/`_deadline`, `tk_kernel_init`/ `_start`/`_set_idle_hook`/`_set_sched_policy`/`_set_preempt_mode`/`_now_ms`, the opaque `tk_thread_t` + `tk_thread_fn` + `tk_thread_attr_t` + enums + `TK_THREAD_STACK`, `tk_thread_get_stats` (gated). The `tk_sleep_ms` inline forwarder (ADR 0002 §8) lives here over `tk_thread_sleep_ms`. Sync objects live in their own Tier-A headers (`tk_mutex.h`/`tk_sem.h`/`tk_cond.h`); time in `tk_time.h`. **→ Tier A-introspect (read-only monitor, `TK_CONFIG_ENABLE_INTROSPECT`):** the thread snapshot + `struct tk_thread_info`, `tk_thread_set_name`. App/trace-facing, so they are introspection surface, not the port contract. **→ Tier B (`tk_kernel_port.h`):** `tk_kernel_tick_preempt`, `tk_kernel_preempt_select`, `tk_kernel_switch_select`, `tk_kernel_start_this_core`, `tk_kernel_trace_switched_in`, `tk_svc_dispatch`, `tk_kernel_fault_contain`. These pair with the `arch_*` contract in `kernel_arch.h`: the port PROVIDES `arch_*`, the kernel PROVIDES these; both belong at the port seam, never in the app header. All take/return raw stack pointers or exception frames and assume the kernel-lock discipline — calling them from app code corrupts the scheduler. **→ Tier C (`tk_kernel_sync.h`):** `struct tk_wait_queue` + `tk_wait_queue_init`, `tk_kernel_wait`, `tk_kernel_wake_one`, `tk_kernel_current_thread`. Consumed by the kernel's own mutex/sem/cond (`tk_sync.c`) AND by an app building a new primitive (a message queue, a barrier). Discipline: `tk_kernel_wait`/`_wake_one` MUST be called holding the kernel lock (`tk_sched_enter`, Tier D) so the object's check-then-block stays atomic — now the discipline and the lock primitive live in the *same visible tier pair*, which is what closes the original leak. **→ Tier D (`tk_kernel_internal.h`):** `struct tk_tcb` layout, the kernel singleton, `tk_sched_enter`/`tk_sched_leave`, the static `tk_kernel_tick`/`tk_kernel_schedule`. Only `kernel/*.c` and the tests include it; it pulls Tier A (enums/handle) + Tier C (the wait-queue the TCB embeds). ## 5. The orthogonal dimension: privilege (`TK_CONFIG_UNPRIV_THREADS`) Within Tier A, an unprivileged thread cannot call everything at runtime. Two sub-classes: - **Runtime-safe `[rt]` (syscall path):** `tk_thread_yield`/`_sleep_ms`/`_exit`/`_join` and the sync pend/post ops — these trap through `tk_svc_dispatch` and run privileged briefly. - **Setup-only `[setup]` (privileged):** `tk_thread_create`/`_spawn`/all `set_*`, `*_set_name` — valid only before `tk_kernel_start` (or from a privileged system thread). Passing a raw `tk_thread_t*` to a kernel call from unprivileged code makes the kernel a **confused deputy**; that path needs a kernel-validated id. > **Open — the tags are documented, not yet annotated.** `tk_kernel.h`'s banner *defines* > the `[rt]`/`[setup]` convention, but individual declarations do not yet carry the > one-word tag. Applying the per-symbol annotation is the remaining ergonomics to-do. ## 6. Config-gated public surface (the one gate idiom) The app API itself flexes with configuration. k2 uses **one** gate mechanism, stated here as the authority: a `TK_FEAT_*` macro is defined **iff** the app opted in (`TK_CONFIG_*`) **and** the arch can do it (`ARCH_HAS_*`). From `kernel/tk_config.h`: ``` TK_FEAT_MPU_GUARD = (ENABLE_MPU_GUARD || UNPRIV_THREADS) && ARCH_HAS_MPU TK_FEAT_UNPRIV = UNPRIV_THREADS && ARCH_HAS_UNPRIV TK_FEAT_ISOLATION = ENABLE_MPU_ISOLATION && TK_FEAT_UNPRIV && ARCH_HAS_MPU TK_FEAT_AFFINITY = ENABLE_AFFINITY && (ARCH_CORE_COUNT > 1) ``` A feature compiled off makes its API **vanish** (not stub). The `ARCH_HAS_*` capability half is declared by each port (defaults in `arch/arch_defaults.h`); which board satisfies which capability is the coverage matrix in [board-contract.md](board-contract.md) §8 — this doc owns the *idiom*, that doc owns the *coverage*. ## 7. Ergonomics — kept, they are proven - **Singleton kernel** (no kernel pointer in any call) — `init → create → start`. ADR [0002](../decisions/0002-multi-instance-kernel.md) generalizes this to N instances while keeping the single-instance calls source-compatible (`[rt]` ops resolve `this_kernel()`). - **Opaque kernel-owned handles** (`tk_thread_t`, `tk_mutex_t`, `tk_sem_t`, `tk_cond_t`) — the freedom-from-interference design; the app holds a handle, never the layout. - **pthread shape** (`spawn`/`attr`/`join`/`detach`, `void*(*)(void*)` entry). ## 8. Residual leak (open) `tk_kernel.h`'s introspection snapshot `struct tk_thread_info` declares `uint8_t base_priority` / `uint8_t state` — the kind-1-as-`uint8_t` anti-pattern ADR 0001 (Addendum 2) forbids for a word-addressed arch. Public-header leak, predates this record; fix to `unsigned char` when the introspection surface is next touched. (Tracked identically in ADR 0001.)