Root 2.1 kernel API
The frozen 34-symbol ABI 1.4 caller contract, refusal codes and process-crash durability limits.
Final contract for ABI 1.4, annotated tag root-2.1. Source citations are relative to its RURAL commit 4e84fec244a29f348946ec006894c51df87290b7; line numbers describe that commit, not the earlier draft worktrees. RN-08 checked the tag target on 9 November 2026; the earlier pre-rewrite citation is superseded. Signature review remains pending after the history rewrite. This document resolves draft decisions from landed code; it does not close the remaining evidence gates recorded in I-01.
Root 2.1 is process-crash durable, NOT power-loss durable. Successful durable operations sync before acknowledging; uncertain publication poisons the handle. The isolated block-device harness is deferred. SIGKILL/torn-write evidence and bounded formal checks do not qualify physical power-loss ordering (src/storage/receipt_log.zig:2626, src/storage/receipt_log.zig:2720, docs/grove/results/R-12b.md:6, docs/grove/results/R-12.md:212).
Frozen surface and caller contract#
Exactly 34 symbols, with the argument/return declarations, pointer nullability, fixed array extents and struct layouts in the cited header frozen verbatim. rural_grove_abi_version() returns 0x00010004; phase-1 builds remain a distinct 26-symbol ABI 1.0 (src/grove_abi.zig:72, include/rural_grove.h:67). The groups contain 5 lifecycle, 1 memory, 7 arena, 11 map, 4 log, 3 CAS/key and 3 storage operations. The previous draft's “Map (12)” heading was incorrect.
| Frozen symbol | Declaration and canonical inventory |
|---|---|
rural_grove_abi_version | include/rural_grove.h:88; dist/grove-v1/ABI-SYMBOLS.txt:1 |
rural_grove_arena_get | include/rural_grove.h:97; dist/grove-v1/ABI-SYMBOLS.txt:2 |
rural_grove_arena_has | include/rural_grove.h:98; dist/grove-v1/ABI-SYMBOLS.txt:3 |
rural_grove_arena_import_redaction | include/rural_grove.h:86; dist/grove-v1/ABI-SYMBOLS.txt:4 |
rural_grove_arena_put | include/rural_grove.h:96; dist/grove-v1/ABI-SYMBOLS.txt:5 |
rural_grove_arena_put_sealed | include/rural_grove.h:99; dist/grove-v1/ABI-SYMBOLS.txt:6 |
rural_grove_arena_redact | include/rural_grove.h:68; dist/grove-v1/ABI-SYMBOLS.txt:7 |
rural_grove_arena_sealed_meta | include/rural_grove.h:100; dist/grove-v1/ABI-SYMBOLS.txt:8 |
rural_grove_buf_free | include/rural_grove.h:93; dist/grove-v1/ABI-SYMBOLS.txt:9 |
rural_grove_cas | include/rural_grove.h:125; dist/grove-v1/ABI-SYMBOLS.txt:10 |
rural_grove_cas_all | include/rural_grove.h:70; dist/grove-v1/ABI-SYMBOLS.txt:11 |
rural_grove_checkpoint | include/rural_grove.h:73; dist/grove-v1/ABI-SYMBOLS.txt:12 |
rural_grove_close | include/rural_grove.h:90; dist/grove-v1/ABI-SYMBOLS.txt:13 |
rural_grove_fsck | include/rural_grove.h:75; dist/grove-v1/ABI-SYMBOLS.txt:14 |
rural_grove_get_key | include/rural_grove.h:126; dist/grove-v1/ABI-SYMBOLS.txt:15 |
rural_grove_last_error | include/rural_grove.h:92; dist/grove-v1/ABI-SYMBOLS.txt:16 |
rural_grove_log_append | include/rural_grove.h:117; dist/grove-v1/ABI-SYMBOLS.txt:17 |
rural_grove_log_head | include/rural_grove.h:119; dist/grove-v1/ABI-SYMBOLS.txt:18 |
rural_grove_log_import | include/rural_grove.h:120; dist/grove-v1/ABI-SYMBOLS.txt:19 |
rural_grove_log_read | include/rural_grove.h:118; dist/grove-v1/ABI-SYMBOLS.txt:20 |
rural_grove_map_diff | include/rural_grove.h:113; dist/grove-v1/ABI-SYMBOLS.txt:21 |
rural_grove_map_drop | include/rural_grove.h:110; dist/grove-v1/ABI-SYMBOLS.txt:22 |
rural_grove_map_empty | include/rural_grove.h:103; dist/grove-v1/ABI-SYMBOLS.txt:23 |
rural_grove_map_fork | include/rural_grove.h:104; dist/grove-v1/ABI-SYMBOLS.txt:24 |
rural_grove_map_get | include/rural_grove.h:105; dist/grove-v1/ABI-SYMBOLS.txt:25 |
rural_grove_map_get_sealed | include/rural_grove.h:111; dist/grove-v1/ABI-SYMBOLS.txt:26 |
rural_grove_map_len | include/rural_grove.h:114; dist/grove-v1/ABI-SYMBOLS.txt:27 |
rural_grove_map_list | include/rural_grove.h:107; dist/grove-v1/ABI-SYMBOLS.txt:28 |
rural_grove_map_list_entries | include/rural_grove.h:69; dist/grove-v1/ABI-SYMBOLS.txt:29 |
rural_grove_map_seal | include/rural_grove.h:109; dist/grove-v1/ABI-SYMBOLS.txt:30 |
rural_grove_map_set | include/rural_grove.h:108; dist/grove-v1/ABI-SYMBOLS.txt:31 |
rural_grove_open | include/rural_grove.h:89; dist/grove-v1/ABI-SYMBOLS.txt:32 |
rural_grove_open_v1 | include/rural_grove.h:71; dist/grove-v1/ABI-SYMBOLS.txt:33 |
rural_grove_scan | include/rural_grove.h:84; dist/grove-v1/ABI-SYMBOLS.txt:34 |
Normative layout: opaque rural_grove_engine; rural_grove_buf is nullable uint8_t *ptr, then size_t len; rural_grove_cas_op is nullable constant key pointer, size_t key_len, int32_t has_expected, uint8_t expected[34], int32_t has_new, uint8_t new_value[34]. Use native C alignment, not packed layouts. Hashes are 34 bytes and canonical leaves 48 bytes (include/rural_grove.h:8, include/rural_grove.h:26, include/rural_grove.h:61).
Calls are serialized per engine. The caller holds a mutex across call plus last_error, an entire paginated scan, and exclusive close. Pointers cover their extents; null byte pointers require zero length. Output objects are valid, aligned and mutually disjoint. Use disjoint input/output storage to satisfy both the general comment and the older CAS-specific restriction. No output borrows an engine. Free each returned buffer exactly once even after failure (including fsck). New APIs clear buffer/handle/more on failure; scan preserves generation. last_error requires a valid handle and cannot diagnose a failed open that published none (include/rural_grove.h:29, include/rural_grove.h:121, src/grove_abi.zig:85, src/grove_abi.zig:371).
Return codes (ABI 1.4)#
| Code / name | Meaning | Retryable | Source |
|---|---|---|---|
0 OK | Success; fsck requires complete and clean (CLI exit 0) | — | include/rural_grove.h:12; src/grove_abi.zig:983 |
1 NOT_FOUND | Missing content/source/target | No, unless caller changes the input/state | src/grove_abi.zig:54 |
2 IMMUTABLE | Immutable operation, including self/unsealed redaction target | No | src/grove_abi.zig:57; src/storage/arena.zig:440 |
3 INVALID_PATH | Invalid record/path/node/key/leaf/sealed metadata, flags or scan arguments | No; correct input | src/grove_abi.zig:58; src/grove_abi.zig:62 |
4 UNKNOWN_FORK | Unknown or consumed fork | No; obtain a live fork | src/grove_abi.zig:59 |
5 CHAIN_BROKEN | Broken receipt chain or malformed v1 import | No | src/grove_abi.zig:60; src/grove_abi.zig:900 |
6 CAS_MISMATCH | Comparison outcome, current value outputs filled; not a failed call | Re-read and deliberately recompute; no blind retry | src/grove_abi.zig:63; include/rural_grove.h:121 |
7 CAPACITY | Oversize, memory, space, segment/log or other capacity limit | No blind retry; operator/resource action | src/grove_abi.zig:64 |
8 ADAPTER | Busy LockUnavailable, ScanChanged, uncertain I/O, unsupported handle operation, other errors, incomplete/refused fsck (CLI exit 3) | Busy: retry after owner releases. ScanChanged: restart generation 0. Others: inspect error/report, no blanket retry | src/grove_abi.zig:65; src/grove_abi.zig:251; include/rural_grove.h:79; src/grove_abi.zig:983 |
9 REDACTED | Redacted content | No | src/grove_abi.zig:56 |
10 CORRUPT | Stored corruption, internal IdentityMismatch, unsupported version or length overflow; fsck corrupt/unacknowledged (CLI exit 6) | No | src/grove_abi.zig:61; src/grove_abi.zig:984 |
11 WRONG_NODE | Valid supplied local node differs from FORMAT identity | Never with the same identity | include/rural_grove.h:23; src/grove_abi.zig:55; src/grove_abi.zig:325 |
Busy and wrong-node are distinct in the same C return space (RF-173 resolved). A failed competing open returns 8 without a handle; wrong-node returns 11. Malformed node input is 3, and stored identity damage is 10. Never treat every code 8 as busy. The ABI has no global last_error for open failures (include/rural_grove.h:39, src/grove_abi.zig:78, src/grove_abi.zig:98).
Refusals and admission#
| Condition | Frozen result | Source |
|---|---|---|
open_v1 on nonempty unmarked root outside admitted bootstrap shape | 10, no legacy fallback | src/grove_abi.zig:112; src/grove_abi.zig:121; src/grove_abi.zig:301 |
Empty unmarked root with LOCK and/or root directory .embedder/ | Init admitted; pending bootstrap may also contain FORMAT.next. Root files GROVE and .cli-lock are unknown entries; init refuses 10 | src/grove_abi.zig:108; src/grove_abi.zig:114 |
.embedder is a file/symlink, or arbitrary unknown root entry | No opaque-namespace admission; init refuses 10 | src/grove_abi.zig:115; src/grove_abi.zig:116 |
open_v1 on unmarked legacy store | 10; explicit open retains unmarked legacy compatibility | src/grove_abi.zig:301; src/grove_abi.zig:309 |
| Valid local node differs from marked node | 11, not retryable busy | src/grove_abi.zig:325 |
| Same-root live owner, same or other process | 8; independent roots may open concurrently | include/rural_grove.h:44; src/grove_abi.zig:252 |
| Invalid LOCK inode after acquisition (non-file or nlink != 1) | 10; acquisition failures remain 8, not all are diagnosed corruption | src/grove_abi.zig:252; src/grove_abi.zig:261; src/grove_abi.zig:65 |
| Missing LOCK on normal open vs read-only fsck | Open may create/sync it; fsck never creates it and reports refused/ABI 8 (CLI exit 3) | src/grove_abi.zig:252; src/grove_abi.zig:298; src/storage/grove_fsck.zig:1523 |
| Invalid acknowledged metadata / no valid slot / equal valid slot generations | 10; no silent legacy downgrade | src/grove_abi.zig:330; src/storage/grove_commit.zig:67; src/storage/grove_commit.zig:80; src/storage/grove_commit.zig:90 |
| RecoveryRequired on marked store | Recovery admission runs; handle published only after successful settlement | src/grove_abi.zig:90; src/grove_abi.zig:329 |
| Destroyed-slot rebuild needing signed approval | Refuse 8; no approval input seam or fabricated receipt | include/rural_grove.h:58; src/grove_abi.zig:326 |
| Checkpoint/scan on legacy compatibility handle | 8, never false success | include/rural_grove.h:82; src/grove_abi.zig:962 |
| fsck nonzero flags / invalid scan kind, limit or flags | 3 | include/rural_grove.h:36; include/rural_grove.h:76; src/grove_abi.zig:968; src/grove_abi.zig:62 |
cas_all count outside 1..16, duplicate key | 3 (InvalidRecord) | src/grove_abi.zig:478; src/grove_abi.zig:483; src/storage/receipt_log.zig:553 |
arena_redact self/unsealed target, missing source/target, redacted target | 2 / 1 / 9 respectively; conflicting link 2; repeat identical link succeeds | src/storage/arena.zig:440; src/storage/arena.zig:453; src/storage/arena.zig:552 |
arena_import_redaction live destination / non-distinct or invalid ciphertext | Live destination immutable 2; helper has separate InvalidRecord/NotFound validation | include/rural_grove.h:85; src/storage/arena.zig:469; src/storage/arena.zig:489 |
Fsck vocabulary and opaque namespace#
Read-only report fields include schema_version, format_version, complete, clean, verdict, embedder_namespace, coverage, counts, scratch, first_failure, commit_slots, truncation flags and findings. Finding fields are path, offset, category, severity, invariant. Retained-list truncation does not change verdict/count semantics (src/storage/grove_fsck.zig:67, src/storage/grove_fsck.zig:98, src/storage/grove_fsck.zig:126).
| Frozen vocabulary / rule | Meaning / ABI result / CLI exit | Source |
|---|---|---|
Verdicts clean, incomplete, corrupt, io_error, unacknowledged, unmarked, unsupported, refused | clean, complete/clean and zero findings -> ABI 0 / CLI 0; corrupt or unacknowledged -> ABI 10 / CLI 6; all others -> ABI 8 / CLI 3 | src/storage/grove_fsck.zig:17; src/grove_abi.zig:982 |
Categories corruption, io_failure, unacknowledged, unmarked, unsupported, lock_unavailable, warning | Stable machine words; severity error or warning | src/storage/grove_fsck.zig:16; src/storage/grove_fsck.zig:67; src/storage/grove_fsck.zig:144 |
| Verdict aggregation | I/O dominates; other non-warning categories assign their verdict in traversal order, except unacknowledged only replaces incomplete | src/storage/grove_fsck.zig:127 |
complete / clean | complete = !unverified; clean requires complete and findings == warnings; warnings do not set first_failure, even with report limit zero | src/storage/grove_fsck.zig:117; src/storage/grove_fsck.zig:137; src/storage/grove_fsck.zig:1580 |
Root .embedder/ | embedder namespace: present, opaque; no opening/enumerating/checking contents; may stay complete/clean | src/storage/grove_fsck.zig:1353; include/rural_grove.h:52 |
Root GROVE / .cli-lock regular files | Ordinary unknown entries: unsupported object class, category unsupported, severity error; incomplete, not clean; ABI 8 / CLI 3 | src/storage/grove_fsck.zig:1403 |
| Unknown namespace / interrupted traversal | Prevents completeness; a fully inspected corrupt store can still be complete, never clean | src/storage/grove_fsck.zig:85; src/storage/grove_fsck.zig:1579 |
| Existing LOCK absent / held / multiply linked | Lock refusal, incomplete and non-clean; ABI 8 / CLI 3; no mutation | src/storage/grove_fsck.zig:1523 |
| Bytes beyond selected COMMIT | unacknowledged, not committed content; ABI 10 / CLI 6 | src/storage/grove_fsck.zig:1269 |
Only the root directory .embedder/ is opaque; similarly named nested objects or a root file/symlink are not namespaces. Within fsck's documented coverage, all non-opaque store bytes are kernel-owned and inspected; unknown root entries prevent a clean verdict. Supplied typed tree roots are checked; fsck does not infer application tree roots or semantic supersession edges from arbitrary blobs/CAS digests (src/storage/grove_fsck.zig:156, src/storage/grove_fsck.zig:1353, src/storage/grove_fsck.zig:1477).
Slot publication, best-effort frames and locks#
A/B slots are independent whole envelopes, version 2 kind 11. Select the valid identity-bound slot with greatest generation; equal valid generations or a present pair with no valid member refuse corruption. One valid slot suffices. Both absent is allowed only for first publication, not normal load (src/storage/grove_commit.zig:40, src/storage/grove_commit.zig:59, src/storage/grove_commit.zig:90). The writer validates generation+1, overwrites the other slot at offset zero without truncation or a rename, then follows:
sync frames and candidate pages -> whole slot overwrite -> file fsync (including fdatasync barrier) -> parent-directory sync -> memory publication -> acknowledgement (src/storage/grove_commit.zig:179, src/storage/grove_commit.zig:195, src/storage/grove_commit.zig:208, src/storage/grove_commit.zig:214, src/storage/receipt_log.zig:2632, src/storage/receipt_log.zig:2644). RuralSlots is bounded evidence for this ordering, with independent-file/checksum/durable-payload assumptions; it is not an unbounded implementation proof (docs/grove/results/F-01.md:7, docs/grove/results/F-01.md:77).
The local durable-best-effort stream appends/syncs a kind-11 frame carrying the next candidate N+1, before publishing its slot. Complete durable but unacknowledged N+1 may be recovered; a torn N+1 falls back while preserving acknowledged N. Durable mode and imported peer streams write zero such frames. Fsck validates their metadata and reports bytes beyond selected COMMIT as unacknowledged (src/storage/receipt_log.zig:2624, src/storage/receipt_log.zig:2708, src/storage/receipt_log.zig:2717, src/storage/receipt_log.zig:2725, src/storage/grove_commit.zig:93, src/storage/grove_fsck.zig:1269). The frozen C open selects explicit durable configuration; the best-effort rule also describes internal storage behavior (src/grove_abi.zig:326).
One lock per store root, held per handle. Rural's root LOCK owns store exclusion, acquired exclusively and nonblocking before classification of FORMAT/bootstrap/legacy data; fsck uses the same existing inode and exclusive fence. Competing opens return busy 8 in both the same and other processes. Legacy compatibility also holds owner.lock against old writers. No lock inode is removed. Close/error unwind explicitly unlocks the owning description before closing it, so fork-inherited descriptors cannot prolong that handle's lock. After close returns, a valid root can be reopened by any process unless another live owner acquired it. An embedder CLI lock only serializes its CLI and never owns the store-use fact (include/rural_grove.h:44, src/grove_abi.zig:244, src/grove_abi.zig:296, src/grove_abi.zig:310, src/grove_abi.zig:365, src/storage/grove_fsck.zig:1523).
Post-tag changes, limits and evidence#
D-R45's change rule: additive only after the tag. A changed symbol gets a _v2 suffix; the old symbol stays. No frozen symbol, argument/struct field, existing enum value, verdict word or category is removed before Root 3. New codes append; additions increment the minor version while major remains 1. The appended WRONG_NODE 11 is part of this freeze, ABI 1.4, not a post-tag addition (include/rural_grove.h:23, src/grove_abi.zig:72; freeze policy recorded in docs/grove/results/I-01.md).
Known limits: the frozen 1.0 binary refusing a format-2 store leaves one empty mode-0600 regular file owner.lock. This is a harness tolerance only; fsck reports it as an unknown root entry, and the operator may delete it.
Known limits: RuralMajority remains a skeleton, outside Root 2.1 proof claims (formal/RuralMajority.tla:6). Chunked writes are not exposed through the GROVE binding; ordinary large puts are not chunked evidence (G-02c result and M-5 transcript, referenced by the collector). R-20 heads journal is post-tag: R-14 measured 470,040 bytes/CAS against 65,536, with idle-host timing and R-21 sync attribution unqualified (docs/grove/results/R-14.md:5, docs/grove/results/R-14.md:169). Power-loss qualification remains deferred.
Run zsh scripts/root21_evidence.zsh <absent-outdir> at the annotated tag commit. It emits EVIDENCE.md/evidence.json with exact commands, actual exits/counts, log hashes, source/archive identities and honest MAIN/reference rows. Set GROVE_WORKTREE for offline fresh GROVE creation/full comparison; ROOT21_DOGFOOD_STORES supplies newline-separated closed roots for read-only fsck, and ROOT21_FORMAL_CACHE supplies a prepopulated verified pinned jar cache. No network download or live-store migration is performed. R-14 numbers and the M-5 transcript are explicitly identified historical references, not relabelled current runs. I-01's retained pre-tag collection does not prove the rewritten tag commit; remaining MAIN items require matching evidence. Tag signature review remains pending. The collector only prints a signing command and never executes it.