Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

.nornir/design.md

edda — design

edda is the nordisk shared-lib samling: the reusable, non-facett library crates lifted out of nornir so they release on their own cadence and are shared by the whole constellation (nornir, korp, holger, skade, znippy, …) without any consumer dragging in nornir's warehouse / embedder / server tree.

Charter

The dependency spine

edda sits mid-stack in the release topo order:

tunnr (tunnr-vm) ─▶ draupnir ─▶ edda (autonom, jera, nornir-*) ─▶ … ─▶ nornir

jera hard-deps draupnir (path) for provisioning; autonom is pinned by nornir. So edda publishes after draupnir and before nornir — the crate-topo sort orders it automatically.

The crates and their seams

crate seam it owns
autonom the completeness gate Gap = Surface − Covered − Allowlist (HARD zero) + the runnable coverage feeder (syn call-graph → the code-that-lacks-tests worklist). The pure engines live in nornir-testmatrix::discover; autonom is the constellation front-end
nornir-testmatrix the pure matrix core: runner (drives nextest/cargo, stall watchdog), discover/coverage (surface enumeration + the gate math), uiplane (UI-plane model). std+serde+anyhow only
nornir-robotui drive (egui_kittest/AccessKit) + observe (state_json) + replay (TOML scenarios). snapshot feature gates the wgpu PNG path so the core stays light
nornir-saga record nav/click/log/error/state events → warehouse → replay as a generated robot test. Toolkit-neutral (egui OR Leptos+Axum). saga_to_ui(events) is the event-model → UI-movement bridge (the inverse of Saga::to_saga_events / nornir's saga_from_ui): folds a stored SagaEvent stream back into a replayable Saga — actionable nav/click events become movements, ordered by capture seq, t_ms clamped non-decreasing, so a round-trip preserves the movement sequence and the Player frame count lines up with the ReplayPlan steps. The ONE primitive both nornir's viz tab and the dwarves tape-recorder pane consume
nornir-bench the run/result envelope + append-only JSONL history; free-form per-repo metrics JSON; single-thread + core-saturation aware; the diff regression gate — BenchDiff::between(baseline, new) folds two runs into per-(bench, metric) MetricDeltas (direction-aware via direction_of/unit_of, skips static rows + Neutral metrics) and answers is_regression(tolerance), failing on a scalar drop OR any test flipped red
nornir-coverage cargo-llvm-cov line/region coverage (the orthogonal "how many lines ran?" to autonom's "is every surface reached?") + the diff regression gate: CoverageDiff::between(old, new) folds two reports into overall + per-crate CoverageDeltas and answers is_regression(tolerance) — the pure CI gate a third consumer wants, baseline-storage left to the owner
nornir-git gix inspection + the pure-Rust git filter-repo analogue (path removal)
nornir-cli-outcome the {command, ok, data, human} CLI result shape — the CLI twin of state_json — plus the process-exit bridge (exit_code/sannr_exit_code/finish) so one outcome drives both the render and std::process::exit
jera the durable job ledger (redb), the no-shell container runtime, and the KVM boot runner — both delegated to draupnir. Plus workspace_sink(workspace) / workspace_ledger_path(workspace) — the app-facing "give me a durable job sink for my app" helper that korp (jobs.rs) and modgunn (tools.rs) had each hand-rolled byte-identically (<WORKSPACE_UPPER>_JOBS env else <temp>/<workspace>_jobs.redb; open the parent as the ledger root; JobSink::noop() on failure), now derived once with the env-var + file literals as the workspace argument
nornir-depgraph the cross-repo dependency graph: cargo-metadata/direct-manifest compute → cross-repo edges + petgraph topo build order, the egui-free dependency-VIEW layout (state_json), and the DepGraphSnapshot ⇄ DepGraphRow persistence fold (to_rows/group_snapshots) so a warehouse stores flat rows and reconstructs snapshots. No warehouse I/O, no egui. testmatrix self-emitter (selftest::emit) records the round-trip verdict
nornir-service the ONE tonic gRPC scaffold three servers used to twin: token_matches (constant-time bearer compare — empty supplied token always fails), auth_interceptor (the with_interceptor bearer factory; empty configured token = open dev mode), workspace_header (metadata reader), resolve_token(flag, env_var, token_file) (the server-secret resolution ladder --flag > env var > <home>/.<app>/token file > open — empty/blank at any rung falls through, never "set to empty"; the three apps' copies differed only in the env-var + file literals, now the two parameters, so korp/dwarves/nornir-server collapse onto it; the ≥16-char / required-token policy stays in each conductor), and the shared serve/serve_with_tls loops (the latter behind the tls feature). App-agnostic — an app supplies its own tonic Router. Also client::bearer(token) -> Result<Option<MetadataValue>, BearerError> — the ONE outbound authorization: Bearer <token> stamp (empty token ⇒ None ⇒ open, a newline/invalid token is a typed error) the leaf embed crates + nornir's client paths consume instead of hand-rolling format!("Bearer …").parse().insert(…). And the pluggable, object-safe AuthProvider seam (auth-provider feature): Principal{subject,scopes,method} + verify_bearer/verify_client_cert/verify_saml_assertion/verify_webauthn_assertion with ProviderError::{Invalid (authoritative reject), Unavailable (try next)}, dispatched in order by verify_any/verify_any_cert. Each MODERN method lives in its own feature-gated nornir-auth-* sub-crate so a build links only the providers it pinned; the trait is object-safe so a future wasmtime-loaded provider slots in unchanged
nornir-flight the ONE generic Apache Arrow Flight transport seam: raw do_put/do_get/do_exchange over RecordBatch behind two schema-agnostic traits (FlightSink/FlightSource), FlightServer composing nornir-service's bearer interceptor + serve loop (auth NOT re-rolled), FlightConn client (bearer + connect_tls), MemFlightStore reference sink. TLS behind the tls feature; the schema + byte-meaning stay with each consumer (build-info stream, graphar-flight, …)
nornir-release the warehouse-free release logic (the L5a canonical split — logic here, cargo metadata/cargo publish orchestration in nornir, dwarves a CLI caller): the source-drift detector (assess/can_skip_republish/diff_source_maps — skip republishing an unchanged crate) and the pure publish-wave planner (plan_waves/topo_phases → deps-first parallel phases, publish=false dropped, Dev edges excluded, cycles surfaced). std-only, own DepKind so no cargo_metadata enters the leaf
nornir-hash the ONE shared content-hash primitive six repos hand-rolled (nornir warehouse blob_store, holger retention + loose-file backends, Skidbladnir archive, skade bench aegir, nornir-guard, build-thing provenance each re-spelled Sha256::new();update;hex(finalize()) + a private hex(&[u8])->String): sha256/sha256_hex (lowercase-hex digest of a slice), sha256_file (streaming, 64 KiB chunks — never loads the whole file), the incremental Sha256Hasher (update/finalize/finalize_hex), and the dependency-free hex_encode/hex_decode round-trip (HexError). Dep-light: sha2 only, hex hand-rolled — a third content-addressed store wants it as-is. testmatrix self-emitter records the FIPS-180 vectors + hex round-trip verdicts
nornir-xtask-support the ONE std-only xtask primitives toolbox four repos' cargo xtask crates hand-rolled byte-identically (publish = false): which (PATH + ~/.cargo/bin), run (spawn → non-zero exit is an Err), copy_dir/fresh_dir/clear_worktree, short_rev (git rev-parse --short HEAD), target_dir (honours $CARGO_TARGET_DIR), workspace_root_of (parent of the xtask manifest dir), and the wasm bootstrap trio ensure_wasm_target/ensure_wasm_bindgen/locked_wasm_bindgen_version (install a CLI matching Cargo.lock — the blank-canvas-mismatch fix). Boxed Send+Sync Result alias so anyhow/Box<dyn Error>/Result<_,String> callers all adapt. std-only; testmatrix self-emitter records the pure-primitive verdicts (target_dir override, copy_dir round-trip, root climb)
nornir-pages the ONE parameterized gh-pages demo/deploy FLOW (publish = false, on nornir-xtask-support): demo_wasm (cargo build release wasm → wasm-bindgen --target web --no-typescript → optional wasm-opt -Oz → cache-busted index.html + .domains/.nojekyll into dist/) and deploy_pages (orphan-worktree squash + push --force <remote> pages:pages). ALL divergence is PagesConfig's 11 knobs (demo_crate, canvas_id, state_export, title, description, pages_url/host/branch, remote_env, commit_label, extra_build_args) + workspace_root. Deliberately shells git for the publish (nornir-git's in-process gix has no orphan-worktree/force-push-branch primitive — routing it there would regress). GOLDEN test pins the full index.html bytes incl. the ?v=<short_rev> cache-bust; testmatrix self-emitter records the render verdicts
nornir-humanize the ONE shared human-readable formatting primitive eight-plus twins across nornir + facett hand-rolled and drifted on units (nornir bin/nornir.rs + facett-pipeline/facett-plandag/facett-file human_bytes/human_size; facett-pipeline/facett-plandag human_count; facett-jobview fmt_elapsed + facett-cicd fmt_dur): bytes (IEC binary 1.5 KiB — the majority canon) + bytes_si (SI decimal 1.5 KB — the documented minority), count (compact SI 3.4M, lowercase k), thousands (grouped digits 1,234,567), duration/duration_ms/duration_secs (340ms/1.5s/2m 03s/1h 02m 03s — the pure core the fmt_elapsed/fmt_dur twins share; their —/…/JobStatus sentinels stay domain-side, L5a), throughput (binary 3.5 MiB/s) and percent/percent_of (12.3%). std-only, ZERO runtime deps; testmatrix self-emitter records the exact-string + boundary (1023 vs 1024) verdicts
nornir-escape the ONE shared markup-escaping primitive SEVEN byte-identical xml_escape/svg_escape copies hand-rolled across four repos (nornir viz/diagram, viz/coverage_metrics_svg, arch/mod, mimir, nornir-introspect; edda nornir-testmatrix coverage SVG — which even carries a "local copy so this module stays PURE std" note; Skidbladnir msi WXS): xml(&str) -> Cow<str> escapes the four entities every twin spells identically — &→&amp;, <→&lt;, >→&gt;, "→&quot; — and returns a borrowed Cow (no allocation) when the input is already clean. Apostrophe is deliberately NOT escaped (zero twins do; SVG here uses double-quoted attributes) so every repoint is byte-identical to the twin's .replace() chain. The five-entity sibling xml_attr(&str) -> Cow<str> ALSO escapes '→&apos; — for the single-quoted-attribute case where a bare apostrophe is not legal; on apostrophe-free input it is byte-identical to xml (a property test pins that, and the apostrophe row is the RED-when-broken gate). Domain-coupled escapers STAY in their conductor (L5a): nornir's escape_typst/escape_typst_text/escape_typst_str (docs markdown→typst, three-way divergent), md_escape/cell (markdown-table, single-site), sanitize_asset (asset-name slug). std-only, ZERO runtime deps; testmatrix self-emitter records the four entity vectors + clean-borrow + RED corrupted-mapping verdicts
nornir-appshell the two PURE, egui-FREE app-shell MODELS every suite app's About card + Tools menu used to hand-roll (publish = false — the facett-about egui widget + the per-app repoints land in a later wave): the Identity card (name/display_name/version/tagline/subtitle/logo_png: Option<Vec<u8>> (LFS-tolerant — holger's embedded bytes are an un-pulled git-lfs pointer)/logo_alt/repository/links/framework/engines) + state_json(open) — the oracle korp/holger/modgunn About surfaces assert (keys open/name/wordmark=display_name/version/tagline/subtitle/logo_alt/has_logo/framework/engines — both wordmark (korp/holger) and display_name (modgunn) so every existing robot reads it unchanged), and the generic ToolMenu/ToolEntry (title/hover/effect_key/view_target/needs_container + keys/find/state_json(last_picked)) — the menu STRUCTURE korp's + modgunn's tools.rs both hand-rolled. Lifts modgunn's about.rs struct Identity verbatim + generalises; each app KEEPS its domain (its enum ToolEffect + entries() builder, its container/demo specs, its ServiceSpec). serde + serde_json only, ZERO other runtime deps; serde round-trippable so a CLI-dumped oracle reads back; testmatrix self-emitter records the Identity oracle + ToolMenu entry/last-pick verdicts
nornir-claude the ONE shared Anthropic Claude Messages-API client (nornir's viz tab + dwarves' ClaudeTab used to twin a bespoke reqwest client each): a provider-agnostic ClaudeTransport seam with a deterministic OfflineTransport stub (no network, no key — replaces dwarves' hand-rolled OfflineClaudeTransport) and a live HttpTransport (feature http, blocking reqwest+rustls). Full credential resolution resolve_credential — env ANTHROPIC_API_KEY (→x-api-key) → env ANTHROPIC_AUTH_TOKEN (→Authorization: Bearer + anthropic-beta: oauth-2025-04-20) → per-session token; NEVER hardcodes a key. send_wire(model, &[WireMsg])->Result<String,String> mirrors the facett-claude pane seam so a UI host adapts with a ~3-line newtype. testmatrix self-emitter records the offline-send + precedence verdicts
nornir-auth-oidc the OIDC / OAuth2 provider: validate a bearer JWT OFFLINE against a cached/pre-provisioned JWKS (airgap) or an opaque token ONLINE via /userinfo, plus the device-authorization-grant (RFC 8628) flow for interactive CLI login. One protocol → Entra ID / Okta / Auth0 / Google / Keycloak / Zitadel. Implements nornir_service::AuthProvider; the jsonwebtoken/JWKS closure links ONLY when a tier pins oidc. Reference impl holger; also korp/dwarves + the shared nornir-auth-client login
nornir-auth-bearer the static Bearer token / API-key provider — the tiny M2M method (CI runners, agents, headless clients). Implements AuthProvider; reuses the shared bearer-secret resolution ladder + token_matches constant-time compare. Feature-gated so an embedded/mini tier ships this ONE provider and nothing heavier
nornir-auth-oauth2 the OAuth 2.0 / 2.1 provider: verify a presented access token via RFC 7662 introspection (SP side) + drive the service-to-service client-credentials grant and interactive auth-code + PKCE flows (opaque tokens + pure-OAuth2 M2M, beyond oidc's offline JWT-vs-JWKS). REUSES the oauth2 crate (no hand-rolled protocol). Implements AuthProvider::verify_bearer; reqwest egress links ONLY behind the online feature
nornir-auth-apikey the hashed static API-key provider: verify a presented key against a store of SHA-256-HASHED keys (hashed-at-rest, distinct from bearer's single plaintext secret) — rotating / multi-tenant stores where plaintext is never persisted. REUSES nornir-hash::sha256_hex + nornir_service::token_matches; no crypto/HTTP of its own, so every tier can pin it cheaply
nornir-auth-mtls the mTLS (client-certificate) provider: map a mutual-TLS leaf-cert Common Name to an identity via verify_client_cert. Cryptographic client-cert trust (the CA chain) is established by the rustls transport; this provider consumes the ALREADY-EXTRACTED CN and does NOT itself path-validate a CA — the tiny zero-crypto transport provider a server pins when it accepts client certs
nornir-auth-saml the SAML 2.0 (enterprise SSO) Service Provider sp (off-by-default feature), pure Rust: verify a browser-posted Response (HTTP-POST binding) — exclusive C14N, enveloped RSA-SHA2 / ECDSA-P-256 XML-DSig against the configured IdP certificate only, XSW-proof reads — then issuer, audience, Destination (required on a signed Response), Recipient, InResponseTo and the time window (skew clamped to 300 s); a refusal status is said by its code only. ADFS / Entra ID / Okta / Ping / Keycloak. The samael path (saml feature) is deleted (A34-14): it was never linked and its defaults were weak
nornir-auth-webauthn the WebAuthn / FIDO2 / passkey (passwordless human login) relying party, pure Rust (p256, no C): pure (default) — signature, challenge, origin (RP), UP/UV flags and counter, the MAC'd session token, PurePasskeyProvider on the AuthProvider seam; ceremony — the server-held per-client challenge store and the assertion/attestation checks Gunnar and nornir-identity link. The ceremony state stays on the server and the verified result (counter included) goes back to the caller. The webauthn-rs path (webauthn feature) is deleted (A34-32): its state carried the subject and it dropped the counter
nornir-auth-client the shared CLIENT-side login — the missing <app> login: OIDC device-authorization-grant (RFC 8628) login (reusing the nornir-auth-oidc device helper), access-token acquire + silent refresh, and a TokenStore seam (default 0600 file store; pluggable to the dwarves encrypted Vault). Any CLI calls login() / current_token() so interactive SSO works the same in korp / holger / dwarves

jera → draupnir consolidation (honesty note)

Containers and KVM used to be re-implemented inside jera. They now route through draupnir's ONE engine: jera::vm::DraupnirVmBootRunner → draupnir::Boot (KVM) and jera::DraupnirContainerBootRunner (containers, feature container-oci = ["draupnir/backend-oci"]). jera's run-to-completion run_container routes through draupnir::container::run_to_completion. The legacy in-crate BollardEngine / BollardBootRunner / ContainerEngine trait have been REMOVED — there is now a single bollard/podman engine, in draupnir. jera's default = [] (no engine linked; the lean std build); the live OCI engine is opt-in behind container-oci. The delegation seams stay daemon-free testable via draupnir's Boot/ContainerControl traits. Design + boundaries: draupnir/.nornir/container-consolidation-plan.md.

Docs

This .nornir/ is the single source of the repo's README and docs (class-tagged): design.md (this file) · guide.md (how to depend on the crates) · for-idiots.md (the beginner tour). The renderer emits the thin README + the full book from these same files — thinning is a render-time class filter, never a manual file-move.