Minerva Secure MCU Export

Minerva export packages a supported SKaiNET compute graph for secure MCU inference through libminerva. Phase one is JVM-first and intentionally narrow: static sequential MLP graphs, Q8 quantization, and the ATmega328P target.

Use this backend when the output must be a Minerva project bundle with compiler input, generated weights, firmware and host harnesses, a manifest, and host verification metadata.

New to this backend? Start with Minerva getting started for the first dry run and real-runtime profile, then return here for the detailed option reference. For the conceptual model, see How Minerva secure MCU export fits.

When to Use Each Export Path

Export path Use it when

StableHLO

You need a portable MLIR/IREE-compatible graph for native, accelerator, or ecosystem compiler flows.

Arduino / C99

You need standalone generated C with static memory allocation and no external secure runtime.

Minerva

You need a secure MCU project bundle that is compiled by libminerva and checked by host verification.

Setup

Inside this repository, use the Minerva module directly:

dependencies {
    implementation(project(":skainet-compile:skainet-compile-minerva"))
}

For a published application, use the SKaiNET BOM and the Minerva artifact:

dependencies {
    implementation(platform("sk.ainet:skainet-bom:0.29.1"))
    implementation("sk.ainet.core:skainet-compile-minerva")
}

Configure libminerva through MinervaExportOptions or environment variables used by the maintained JVM sample:

export MINERVA_COMPILER_SCRIPT=/opt/libminerva/compiler/minerva_compile.py
export MINERVA_RUNTIME_ROOT=/opt/libminerva
export MINERVA_CALIBRATION_NPZ=/secure/project/calibration.npz
export MINERVA_KEY_FILE=/secure/project/device.key
export MINERVA_RUN_CMAKE=true
export MINERVA_RUN_CTEST=true
export MINERVA_HOST_TOLERANCE=1.0
export MINERVA_HOST_ADAPTER_SOURCE=/secure/project/minerva_runtime_adapter.c
export MINERVA_HOST_INCLUDE_DIRS=/secure/project/minerva-host-secrets

MINERVA_KEY_FILE and the generated include/secrets.example.h are placeholders for integration. Do not commit real device keys or derived secrets. For host verification against libminerva, MINERVA_HOST_INCLUDE_DIRS can point at an untracked directory that contains a host-only secrets.h defining MNV_DEVICE_KEY.

Compatibility Matrix

Area Phase-one support

Host platform

JVM export path.

Target

MinervaTarget.ATMEGA328P.

Quantization

MinervaQuantization.Q8.

Graph shape

Static, single-path, sequential MLPs.

Tensor shapes

Fully known rank-2 shapes.

Layer pattern

Input → MatMul → Add? → activation?, repeated in sequence.

Activations

Relu, Sigmoid, and Tanh after a dense layer.

Out of scope

CNNs, attention blocks, recurrent models, dynamic shapes, branching graphs, transformers, and arbitrary imported operator sets.

model.npz stores activation names as scalar NumPy Unicode arrays (relu, sigmoid, tanh, or linear) because the current libminerva compiler reads layer_i_act as a string.

Export API

The public entry point is MinervaExportFacade. The facade accepts an already-built ComputeGraph, or it can record a representative forward pass for compatible SKaiNET models.

import sk.ainet.compile.minerva.MinervaExportFacade
import sk.ainet.compile.minerva.MinervaExportOptions

val options = MinervaExportOptions(
    outputDir = "build/minerva",
    projectName = "TinySecureMlp",
    compilerScript = "/opt/libminerva/compiler/minerva_compile.py",
    runtimeRoot = "/opt/libminerva",
    calibrationNpz = "/secure/project/calibration.npz",
    keyFile = "/secure/project/device.key"
)

val result = MinervaExportFacade().exportGraph(graph, options)
val bundle = result.requireSuccess()
println(bundle.outputDir)

If compilerScript is missing, export still performs compatibility checks, Minerva lowering, and NPZ schema creation before returning a typed compiler prerequisite failure. That makes local validation possible before libminerva is installed.

Generated Project Layout

A successful export writes a project directory under outputDir/projectName:

build/minerva/TinySecureMlp/
  manifest.json
  generated/
    model.npz
    weights.c
  include/
    weights.h
    secrets.example.h
  host/
    CMakeLists.txt
    main.c
    runtime_adapter.example.c
    reference-input.txt
    reference-output.txt
    observed-output.txt   # optional output from a real host run
  firmware/
    main.c

The manifest records the target, quantization, libminerva root, compiler command summary, NPZ schema version, layer count, reference fixture paths, generated files, and SHA-256 fingerprints for generated artifacts. secrets.example.h contains placeholder values only.

Manifest Provenance

manifest.json is the release handoff record for Minerva export. It includes a generatedFileSha256 object keyed by generated project path, covering artifacts such as generated/model.npz, generated/weights.c, include/weights.h, host fixtures, host harness sources, and firmware examples.

Use those fingerprints to audit that the source model, Kotlin export, libminerva compiler output, host verification fixtures, and firmware handoff all refer to the same generated bundle. Compiler key-file arguments are redacted from the command summary, and real device key material must stay outside the generated bundle.

For imported or trained models, keep the original source artifact or training metadata next to the manifest in your release evidence. The Minerva manifest fingerprints the generated Minerva bundle; it is not a replacement for source-model provenance.

Host Verification

Host verification always checks the package structure, generated weight files, model.npz integrity, and placeholder secret hygiene. It also writes and validates deterministic host/reference-input.txt and host/reference-output.txt fixtures.

The generated host harness has a stable adapter ABI:

int minerva_run_inference(const float *input, int input_count, float *output, int output_count);

host/runtime_adapter.example.c implements that ABI against the current libminerva runtime symbols: mnv_init, mnv_seed_prng, mnv_run_with_model, and mnv_verify_output_with_key. The adapter keeps compile-time switches for runtimes that expose the older mnv_run / mnv_verify_output names. It converts SKaiNET’s normalized float fixtures to libminerva Q8 activation buffers and converts Q8 outputs back to floats for parity comparison.

Copy the adapter outside the generated bundle when product-specific scaling or entropy seeding needs local edits, then point CMake at the copied source. This keeps the generated host harness stable while leaving runtime policy in one reviewable adapter file.

Use these metadata keys to opt into external host checks:

metadata = mapOf(
    MinervaHostVerificationMetadata.RUN_CMAKE_BUILD to "true",
    MinervaHostVerificationMetadata.RUN_CTEST to "true",
    MinervaHostVerificationMetadata.HOST_OUTPUT_PATH to "host-output.txt",
    MinervaHostVerificationMetadata.HOST_ADAPTER_SOURCE to "/secure/project/minerva_runtime_adapter.c",
    MinervaHostVerificationMetadata.HOST_INCLUDE_DIRS to "/secure/project/minerva-host-secrets"
)

RUN_CMAKE_BUILD configures and builds host/CMakeLists.txt. RUN_CTEST runs the packaged CTest smoke test. HOST_OUTPUT_PATH lets a real host run write comma- or whitespace-separated float outputs that are compared with the SKaiNET reference output using hostVerificationTolerance.

HOST_OUTPUT_PATH is optional when the host run writes the default host/observed-output.txt file. The generated CMake can build a checkout-style libminerva runtime from runtimeRoot when that directory contains CMakeLists.txt. Include, library directory, and library values are passed to CMake as semicolon-separated lists, matching CMake list syntax; use HOST_LIBRARY_DIRS and HOST_LIBRARIES only when linking an already-built runtime.

Local CI recipe:

./gradlew :skainet-compile:skainet-compile-minerva:jvmTest
./gradlew :skainet-compile:skainet-compile-minerva:minervaHostVerification \
  -Pminerva.hostVerification.enabled=true \
  -Pminerva.runtimeRoot="$MINERVA_RUNTIME_ROOT" \
  -Pminerva.compilerScript="$MINERVA_COMPILER_SCRIPT" \
  -Pminerva.calibrationNpz="$MINERVA_CALIBRATION_NPZ" \
  -Pminerva.keyFile="$MINERVA_KEY_FILE" \
  -Pminerva.hostVerification.tolerance="${MINERVA_HOST_TOLERANCE:-1.0}" \
  -Pminerva.hostVerification.hostAdapterSource="$MINERVA_HOST_ADAPTER_SOURCE" \
  -Pminerva.hostVerification.hostIncludeDirs="$MINERVA_HOST_INCLUDE_DIRS"

The Gradle minervaHostVerification task is gated. It only runs when minerva.hostVerification.enabled=true, minerva.runtimeRoot, and minerva.compilerScript are present. When enabled, it runs jvmTest and runMinervaTinyMlpSample with MINERVA_RUN_CMAKE=true and MINERVA_RUN_CTEST=true by default. Override -Pminerva.hostVerification.runCmakeBuild=false or -Pminerva.hostVerification.runCTest=false only for runtime bring-up. The default parity tolerance remains 1e-3; the real checkout profile sets MINERVA_HOST_TOLERANCE=1.0 by default because current libminerva Q8 host outputs are useful as a runtime smoke proof but are not yet numerically close to the SKaiNET float reference.

For a local checkout proof, the helper below creates an untracked key, calibration archive, host-only secrets.h, and host-only AVR pgmspace.h compatibility shim under build/minerva-real-runtime-profile, then runs the gated verification task with CMake and CTest enabled:

MINERVA_RUNTIME_ROOT=/opt/libminerva ./scripts/run-minerva-real-runtime-profile.sh

Firmware Integration

The generated firmware example intentionally contains integration placeholders. Use the host adapter as the reference for the pinned libminerva public API, then move product-specific entropy seeding, input scaling, and secret provisioning into private firmware code before flashing.

Before flashing firmware:

  • Replace secrets.example.h with a private header outside the repository.

  • Confirm the pinned libminerva checkout still exposes the public API used by host/runtime_adapter.example.c.

  • Re-run host verification after any compiler, runtime, calibration, or key change.

  • Keep manifest.json with release artifacts so the compiler command and schema version remain auditable.

Model Source Workflow

Minerva export starts from a supported SKaiNET ComputeGraph. That graph can come from the Kotlin DSL, a traced forward pass, a hand-built graph, an imported model, or any other path that preserves the phase-one static MLP contract.

For ONNX inputs, use the existing ONNX loader path as an inspection step before constructing a compatible SKaiNET graph. The first phase does not include a general ONNX-to-Minerva importer; unsupported imported operators should be rejected before export. After export, keep manifest.json with the source-model artifact or training metadata so reviewers can compare provenance with the generated Minerva bundle fingerprints.

import kotlinx.io.asSource
import sk.ainet.io.onnx.OnnxLoader
import java.io.File

suspend fun loadOnnxForMinerva(path: String) {
    val loaded = OnnxLoader.fromModelSource {
        File(path).inputStream().asSource()
    }.load()
    val graph = loaded.proto.graph ?: error("ONNX model has no graph")

    val ops = graph.node.map { it.opType }.toSet()
    require(ops.all { it in setOf("MatMul", "Gemm", "Add", "Relu", "Sigmoid", "Tanh") }) {
        "ONNX graph contains operators outside the Minerva phase-one scope: $ops"
    }

    // Convert the inspected static MLP to a SKaiNET ComputeGraph, then call MinervaExportFacade.
}

Maintained JVM Sample

The maintained sample is sk.ainet.compile.minerva.examples.MinervaTinyMlpExportSample in the Minerva module. It builds a tiny two-layer MLP, reads Minerva paths from environment variables, invokes the export facade, and prints bundle and verification status.

Run the sample after configuring libminerva:

./gradlew :skainet-compile:skainet-compile-minerva:jvmTest
./gradlew :skainet-compile:skainet-compile-minerva:runMinervaTinyMlpSample

Run the richer secure MCU examples:

./gradlew :skainet-compile:skainet-compile-minerva:runMinervaSecureMcuExamples
./gradlew :skainet-compile:skainet-compile-minerva:runMinervaSecureMcuExamples \
  -Pminerva.example=safety-guard

sensor-classifier mirrors libminerva’s ATmega sensor-classification shape with eight input features and four output classes. safety-guard shows a smaller health-decision classifier with three output classes. Both examples are dry-run friendly and switch to real compiler execution when the normal MINERVA_* runtime settings are present.

Without MINERVA_COMPILER_SCRIPT, the sample task runs a dry validation through compatibility, lowering, and in-memory NPZ generation. Configure the runtime with Gradle properties or matching environment variables to run the real compiler and host verification:

./gradlew :skainet-compile:skainet-compile-minerva:runMinervaTinyMlpSample \
  -Pminerva.compilerScript="$MINERVA_COMPILER_SCRIPT" \
  -Pminerva.runtimeRoot="$MINERVA_RUNTIME_ROOT" \
  -Pminerva.calibrationNpz="$MINERVA_CALIBRATION_NPZ" \
  -Pminerva.keyFile="$MINERVA_KEY_FILE" \
  -Pminerva.hostVerification.tolerance="${MINERVA_HOST_TOLERANCE:-1.0}" \
  -Pminerva.hostVerification.runCmakeBuild=true \
  -Pminerva.hostVerification.runCTest=true

For CI, prefer the gated verification profile:

./gradlew :skainet-compile:skainet-compile-minerva:minervaHostVerification \
  -Pminerva.hostVerification.enabled=true \
  -Pminerva.compilerScript="$MINERVA_COMPILER_SCRIPT" \
  -Pminerva.runtimeRoot="$MINERVA_RUNTIME_ROOT"

The sample graph is covered by MinervaTinyMlpExportSampleTest, which validates compatibility, lowering, and NPZ generation without requiring real device keys.

Troubleshooting

Symptom Fix

minerva.compiler.script_missing

Set MinervaExportOptions.compilerScript or MINERVA_COMPILER_SCRIPT.

minerva.compiler.runtime_root_not_found

Point runtimeRoot or MINERVA_RUNTIME_ROOT at the libminerva checkout or install directory.

Compatibility fails for an unsupported operation

Reduce the graph to the phase-one MLP pattern, or use StableHLO for a general compiler flow.

CMake or CTest verification fails

Inspect host/build/cmake-configure.log, host/build/cmake-build.log, or host/build/ctest.log; confirm CMake is installed, secrets.h is available from MINERVA_HOST_INCLUDE_DIRS, and the generated host harness is linked against the pinned libminerva runtime.

Secret leak check fails

Remove real secrets from generated files and regenerate the bundle. Only placeholders belong in source control.