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 (the layering law these tiers serve), board-contract.md (who calls the Tier-B port contract), ADR 0001 (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/<cpu>)

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/<cpu> 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 §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 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.)