EU AI Act Article 6 applies to edge devices. On-device inference is not exempt from transparency and record-keeping obligations. Mobile AI systems making decisions that affect individuals require the same evidence chain as cloud-hosted models. A healthcare triage app on a tablet, a credit scoring model on a banking app, or an agricultural advisory system on a field device -- all fall within scope if they affect persons in the EU.

Contents

1. Why Mobile Edge Needs Attestation 2. Android SDK Quick Start 3. Device Context Attestation 4. Write-Ahead Log on Android 5. Buffer Tuning for Mobile Networks 6. Clearing Level Selection for Bandwidth 7. Multi-Model Edge Pipelines 8. Cross-Platform Parity (Android + iOS) 9. Regulatory Mapping 10. Quick Reference 11. Quick Start (Full Example)

Who this is for: Mobile AI developers deploying on-device inference (TFLite, ONNX, MediaPipe, Core ML), Android/Kotlin and iOS/Swift developers, edge computing teams managing device fleets, and compliance teams responsible for AI systems running on mobile or embedded hardware.

1. Why Mobile Edge Needs Attestation

Mobile AI is not a special case. It faces the same regulatory obligations as cloud AI. EU AI Act Article 6 covers AI systems regardless of deployment location. The regulation applies to the system, not the infrastructure.

Healthcare triage apps on Android tablets. Agricultural advisory models on field devices. Financial scoring on mobile banking apps. Insurance claim assessment on adjuster phones. All of these make decisions that affect individuals, and all require evidence of what the model decided, when, and on what device.

The difference is the infrastructure. Mobile environments impose constraints that cloud deployments do not face:

SWT3 addresses these constraints with offline-first architecture: hash locally, buffer locally, flush when connected. The witness payload is small (200 bytes to 2KB depending on clearing level), the write-ahead log survives process death, and the flush strategy adapts to network conditions.

Key principle: Raw inference data never leaves the device. Only cryptographic hashes and metadata are transmitted. This satisfies data minimization requirements (GDPR Article 5(1)(c)) while maintaining a complete evidence chain for the AI system's behavior.

2. Android SDK Quick Start

The Kotlin SDK provides the same witness primitives as the Python and TypeScript SDKs, with Android-specific optimizations for lifecycle management, storage, and network awareness.

Installation

// build.gradle.kts
dependencies {
    implementation("io.tenova:swt3-ai:0.6.2")
}

Basic Witness

import io.tenova.swt3.WitnessClient
import io.tenova.swt3.WitnessConfig

val witness = WitnessClient(WitnessConfig(
    endpoint = "https://sovereign.tenova.io",
    apiKey = "axm_live_...",
    tenantId = "YOUR_TENANT_ID",
    clearingLevel = 2,  // Bandwidth-friendly default
))

val result = witness.wrap(
    prompt = userInput,
    response = modelOutput,
    modelId = "tflite-classifier-v3",
    provider = "on-device",
)
// result.fingerprint is a 12-char hex anchor
// WAL persists to local storage automatically

The wrap() call hashes the prompt and response locally using SHA-256, generates a fingerprint using the locked SWT3 formula, and appends the witness payload to the local write-ahead log. When the device has connectivity, buffered payloads flush to the server automatically.

AI-INF.1 -- Inference Provenance

Every wrap() call generates an AI-INF.1 witness anchor. The fingerprint formula is identical across all 9 SDK languages, ensuring cross-platform verification.

Assessor Tip

Request the device's WAL directory listing to verify that witness payloads accumulate during offline periods. Compare WAL entry count against server-side anchor count after a flush cycle to confirm no evidence gaps.

3. Device Context Attestation

Every mobile witness can include device context via DeploymentContext. This proves where and on what hardware the inference ran. An assessor can verify that a healthcare triage model ran on a certified device class, not on a desktop emulator or rooted phone.

import io.tenova.swt3.DeploymentContext

val context = DeploymentContext(
    deviceModel = android.os.Build.MODEL,
    osVersion = "Android ${android.os.Build.VERSION.RELEASE}",
    chipType = android.os.Build.HARDWARE,
    runtimeVersion = "Kotlin ${KotlinVersion.CURRENT}",
)

val result = witness.wrap(
    prompt = userInput,
    response = modelOutput,
    modelId = "tflite-classifier-v3",
    provider = "on-device",
    deploymentContext = context,
)

The deployment context is included in the witness payload at clearing levels 0 and 1. At clearing level 2, only the device model is retained. At clearing level 3, device context is omitted entirely.

Why Device Context Matters

4. Write-Ahead Log on Android

The write-ahead log (WAL) persists witness payloads to $TMPDIR/swt3-wal/{tenantId}.wal as JSONL (one JSON object per line). On Android, this maps to the app's internal storage directory (context.filesDir), which is private to the application and survives across app updates.

Key Behaviors

Storage budget: At clearing level 2 (~400 bytes per entry), a 5MB WAL holds approximately 12,500 witness payloads. At one inference per second, that covers over 3 hours of continuous offline operation before rotation begins.

WAL Location on Android

// Default WAL path on Android
// /data/data/com.yourapp/files/swt3-wal/{tenantId}.wal
//
// Access via:
val walDir = File(context.filesDir, "swt3-wal")
// WAL files are plain JSONL -- one witness payload per line

5. Buffer Tuning for Mobile Networks

Default buffer settings are optimized for always-connected servers. Mobile environments need different tuning to balance evidence freshness against battery life and network efficiency.

ParameterServer DefaultMobile RecommendedRationale
Flush interval30 seconds300 secondsReduce network wake-ups, save battery
Batch size10 payloads50 payloadsFewer, larger uploads reduce TCP overhead
Dead-letter limit1,000 payloads5,000 payloadsMobile accumulates more during offline periods
Retry backoffExponential, max 60sExponential, max 600sAvoid draining battery on repeated failures

WiFi-Aware Flushing

Check connectivity before draining the buffer. On metered connections, defer the flush unless the WAL is approaching rotation.

import android.net.ConnectivityManager
import android.net.NetworkCapabilities

fun shouldFlush(cm: ConnectivityManager, walSizeBytes: Long): Boolean {
    val network = cm.activeNetwork ?: return false
    val caps = cm.getNetworkCapabilities(network) ?: return false

    // Always flush on WiFi
    if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) return true

    // On cellular, only flush if WAL is above 80% of rotation threshold
    if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
        return walSizeBytes > 4_000_000L  // 80% of 5MB
    }

    return false
}
AI-LOG.1 -- Logging Completeness

The WAL ensures zero evidence gaps even during extended offline periods. Every inference is logged locally before the model response is returned to the caller. The flush strategy determines latency to the server, not completeness of the record.

6. Clearing Level Selection for Bandwidth

Clearing levels control how much metadata is included in each witness payload. On bandwidth-constrained mobile networks, the right clearing level can reduce data transfer by 10x.

LevelNamePayload SizeIncludedMobile Use Case
CL0Analytics~2 KBFull metadata, model ID, device context, all hashes, observationsWiFi-connected devices, development
CL1Standard~1.5 KBModel ID, reduced context, factor hashesConnected tablets, kiosk devices
CL2Sensitive~400 bytesModel ID only, no context detailsField devices, cellular connections
CL3Classified~200 bytesFactors only, model ID hashedAir-gapped devices, high-security mobile

Recommendation: Use CL2 as the default on mobile (400 bytes vs 2 KB = 5x bandwidth reduction). Upgrade to CL0 when on WiFi for richer evidence. The fingerprint is identical at all clearing levels -- only the surrounding metadata changes.

// Dynamic clearing level based on network type
val clearingLevel = when {
    isOnWifi(connectivityManager) -> 0   // Full metadata on WiFi
    isOnCellular(connectivityManager) -> 2  // Minimal on cellular
    else -> 3  // Offline or unknown -- factors only
}
Bandwidth Impact

A device performing 1,000 inferences per day at CL2 transmits approximately 400 KB per flush. At CL0, the same volume would require 2 MB. Over a fleet of 10,000 devices, that difference is 4 GB vs 20 GB per day.

Assessor Tip

Verify that the clearing level configuration matches the data classification of the AI system. A healthcare triage model should operate at CL2 or CL3 by default. If CL0 is used, confirm that the additional metadata does not contain protected health information.

7. Multi-Model Edge Pipelines

On-device pipelines often chain multiple models. An image classifier feeds into a text extractor, which feeds into a sentiment analyzer. Each model in the chain makes a decision that should be witnessed independently, but the chain itself should be traceable as a single unit of work.

Use auto-chaining to link all witnesses in a pipeline with a shared cycle_id:

// All witnesses in this block share the same cycle_id
witness.chain("field-inspection-pipeline") { ctx ->
    val classResult = witness.wrap(
        prompt = imageInput,
        response = classOutput,
        modelId = "image-classifier-v2",
    )
    val textResult = witness.wrap(
        prompt = classOutput,
        response = extractedText,
        modelId = "text-extractor-v1",
    )
    val sentResult = witness.wrap(
        prompt = extractedText,
        response = sentiment,
        modelId = "sentiment-v2",
    )
    // ctx.cycleId links all three anchors
    // Forensic reconstruction can replay the entire pipeline
}

The chain label ("field-inspection-pipeline") becomes a queryable tag in the forensic timeline. An assessor can reconstruct the full decision path for any individual inference by querying the cycle ID.

AI-CHAIN.1 -- Decision Chain Provenance

Auto-chaining creates a linked sequence of witness anchors sharing a single cycle ID. Each anchor in the chain is independently verifiable, but the chain itself proves the order and completeness of the pipeline execution.

8. Cross-Platform Parity (Android + iOS)

SWT3 maintains cross-platform parity for mobile edge attestation. The fingerprint formula is identical across all SDK languages, verified against shared test vectors.

CapabilityAndroid (Kotlin)iOS (Swift)
Fingerprint formulaIdentical -- cross-language parity verifiedIdentical -- cross-language parity verified
Write-ahead logApp internal storage (JSONL)On-device buffer (JSONL)
Core ML witnessNot applicablewitnessPrediction() -- Swift only
TFLite witnesswrap() patternNot applicable
ONNX RuntimeSupported via wrap()Supported via wrap()
MediaPipeSupported via wrap()Supported via wrap()
Payload signingHMAC-SHA256HMAC-SHA256 + Secure Enclave P-256
DeploymentContextFull parityFull parity
Auto-chainingSupportedSupported
Clearing levels0, 1, 2, 30, 1, 2, 3

For Apple-specific details including Core ML prediction witnessing, Secure Enclave signing, and spatial provenance for visionOS, see the Edge Attestation Guide.

Cross-platform verification: An anchor generated by the Kotlin SDK on a Pixel 8 can be verified by the Swift SDK on an iPhone, or by the Python SDK on a server. The fingerprint formula is locked and produces identical output across all 9 SDK languages for the same input.

9. Regulatory Mapping

The following SWT3 procedures are directly relevant to mobile edge AI deployments. Each procedure maps to specific regulatory obligations that apply regardless of whether the AI system runs in the cloud or on a device.

ProcedureNameMobile Relevance
AI-INF.1Inference ProvenanceEvery on-device prediction generates a witness anchor proving what the model decided and when
AI-MDL.1Model IntegrityVerify the model file has not been tampered with on the device (hash comparison against known-good checksum)
AI-HW.1Hardware AttestationDevice context proves the inference ran on specific hardware, not an emulator or unauthorized device class
AI-LOG.1Logging CompletenessWAL ensures no gap in the evidence record, even during extended offline periods
AI-COST.1Resource ConsumptionOn-device compute time, battery impact, memory usage -- resource governance for edge inference
AI-GRD.1Guardrail ActivationOn-device content filters, safety classifiers, and output validation running before the response reaches the user
AI-HITL.1Human ReviewHuman override of mobile AI decisions, review latency, reviewer identity binding
EU AI Act Mapping

Article 9 (risk management) requires documented evidence of how risks are identified and mitigated -- AI-INF.1 + AI-GRD.1 provide this. Article 12 (record-keeping) requires automatic logging of AI system operations -- AI-LOG.1 + the WAL satisfy this. Article 14 (human oversight) requires mechanisms for human intervention -- AI-HITL.1 attests to override events.

For model-specific compliance patterns including quantization witnessing (AI-MDL.7), distillation provenance, and the RAG+SLM evidence chain, see the SLM Compliance Evidence Guide.

10. Quick Reference

QuestionProcedure
How do I prove what my on-device model decided?AI-INF.1 -- wrap every inference
How do I verify the model file was not tampered with?AI-MDL.1 -- hash the model binary on load
How do I prove which device ran the inference?AI-HW.1 -- include DeploymentContext
How do I handle offline periods without evidence gaps?AI-LOG.1 -- WAL persists locally, flushes on reconnect
How do I track battery and compute cost of AI features?AI-COST.1 -- resource consumption witnessing
How do I attest that content filters ran before output?AI-GRD.1 -- guardrail activation witness
How do I prove a human reviewed the AI decision?AI-HITL.1 -- human review witness with reviewer binding
How do I trace a multi-model pipeline as one unit?AI-CHAIN.1 -- auto-chaining with shared cycle ID

11. Quick Start (Full Example)

Complete Kotlin example showing configuration, device context, single inference witness, chained pipeline, and flush.

import io.tenova.swt3.WitnessClient
import io.tenova.swt3.WitnessConfig
import io.tenova.swt3.DeploymentContext
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities

class InferenceWitness(private val appContext: Context) {

    private val connectivityManager =
        appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    private val witness = WitnessClient(WitnessConfig(
        endpoint = "https://sovereign.tenova.io",
        apiKey = "axm_live_...",
        tenantId = "YOUR_TENANT_ID",
        clearingLevel = dynamicClearingLevel(),
        flushInterval = 300_000L,  // 5 minutes
        batchSize = 50,
    ))

    private fun dynamicClearingLevel(): Int {
        val network = connectivityManager.activeNetwork ?: return 3
        val caps = connectivityManager.getNetworkCapabilities(network) ?: return 3
        return when {
            caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 0
            caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> 2
            else -> 3
        }
    }

    private fun deviceContext() = DeploymentContext(
        deviceModel = android.os.Build.MODEL,
        osVersion = "Android ${android.os.Build.VERSION.RELEASE}",
        chipType = android.os.Build.HARDWARE,
        runtimeVersion = "Kotlin ${KotlinVersion.CURRENT}",
    )

    // Single inference witness
    fun witnessInference(prompt: String, response: String, modelId: String): String {
        val result = witness.wrap(
            prompt = prompt,
            response = response,
            modelId = modelId,
            provider = "on-device",
            deploymentContext = deviceContext(),
        )
        return result.fingerprint  // 12-char hex anchor
    }

    // Chained pipeline witness
    fun witnessPipeline(imageBytes: ByteArray) {
        witness.chain("field-inspection") { ctx ->
            val classification = runClassifier(imageBytes)
            witness.wrap(
                prompt = imageBytes.toString(),
                response = classification,
                modelId = "image-classifier-v2",
            )

            val extractedText = runOcr(classification)
            witness.wrap(
                prompt = classification,
                response = extractedText,
                modelId = "text-extractor-v1",
            )

            val assessment = runAssessment(extractedText)
            witness.wrap(
                prompt = extractedText,
                response = assessment,
                modelId = "assessment-v3",
            )
            // All three anchors share ctx.cycleId
        }
    }

    // Call when app moves to background or on WiFi connect
    fun flushIfReady() {
        val network = connectivityManager.activeNetwork ?: return
        val caps = connectivityManager.getNetworkCapabilities(network) ?: return
        if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
            witness.flush()
        }
    }
}

Full SDK documentation: sovereign.tenova.io/docs

Create a free account: sovereign.tenova.io/signup