NAOMS Devlog

Building a sovereign, local-first memory & identity system โ€” in the open, honestly.

One Command, Three Platforms โ€” and the Native-Library Gymnastics Underneath

The same shared Rust core โ€” local database, cryptography, signing engine, on-device speech, and optionally a local language model โ€” builds into a self-contained, on-device daemon on Mac, Linux, and Android from one command family. The twist is that the same library links its math and ML stack differently on each operating system, so packaging it self-contained takes a different trick per OS. And the three forms differ by maturity: a notarized double-click app on Mac, a self-contained daemon tarball on Linux, an on-device app under active hardening on Android.

Technology Architect free July 5, 2026ยท7 min readยทdistribution
TL;DR NAOMS is one memory system โ€” your local database, your cryptography, the signing engine, on-device speech, and optionally a language model that runs on your own hardware. From one command family we build it for Mac, Linux, or Android (or all of them at once), and on each platform the output is a self-contained, on-device daemon: no server in the middle, nothing phoning home to run. The genuinely interesting engineering is that the same shared Rust library links its native math and ML stack differently on every operating system, so packaging it self-contained needs a different trick per OS โ€” a static-link on Mac, a full shared-library closure staged and re-verified on Linux, a cross-compile plus a bundled C++ runtime and 16 KB page alignment on Android. One pipeline, three platforms, three real daemons โ€” but the FORM differs by maturity. Only the Mac build is a notarized, celebrated double-click app. Linux ships a self-contained daemon tarball today, not a polished desktop app. Android is shipped and running, but under an active hardening effort. We say which is which, plainly.

The promise is small to state and large to keep: your whole memory system, installable and self-contained, on the three computers you actually use โ€” and on each of them it runs entirely on your own device, with no server in the middle.

Under the hood there is one shared core, written in Rust. It holds the local database, the cryptography, the signing engine that authorizes every sensitive action, the on-device speech that turns your voice into text, and โ€” optionally โ€” a language model that runs on your own hardware rather than someone else's cloud. That core compiles into a single native shared library. One command family takes that library and produces, for Mac, Linux, or Android (or all three at once), a self-contained daemon: the background process that is NAOMS on that machine.

If that were the whole story it would be a footnote. The interesting part is that the same shared library links its native math and machine-learning stack โ€” the numerical kernels underneath the speech and language models, a family of libraries commonly called ggml โ€” differently on each operating system. Which means the one hard problem of shipping self-contained software โ€” making sure every native dependency the program needs is present and findable on a machine that has never seen your toolchain โ€” has a different shape on each of the three. Here is how each one goes.

flowchart TD
  CORE["Shared Rust core
local DB ยท cryptography ยท signing engine
on-device speech ยท optional language model"] CORE --> LIB["One native shared library
(links the ggml math/ML stack)"] LIB --> PIPE{"One dist command family
mac ยท linux ยท android ยท all"} PIPE --> MAC["macOS output"] PIPE --> LNX["Linux output"] PIPE --> AND["Android output"] MAC --> MACN["ggml is STATICALLY LINKED
into the one library โ€” almost
nothing extra to bundle"] MACN --> MACD["Notarized, stapled double-click app
+ signing binary + first-run
quarantine helper
self-contained on-device daemon"] LNX --> LNXN["ggml stays as separate shared libs
with NO embedded search path โ€”
walk the dependency closure,
stage it all, RE-VERIFY or fail"] LNXN --> LNXD["Self-contained tarball:
runtime + library + signing engine
+ installer + launcher entry
self-contained on-device daemon"] AND --> ANDN["Cross-compile for 64-bit ARM,
bundle the matching C++ runtime lib;
align to 16 KB pages, store uncompressed"] ANDN --> ANDD["Signed APK embedding the daemon
self-contained on-device daemon"]

Mac: let the linker fold it all inward

On macOS the toolchain does the hard part for us. When the shared library is built, the entire ggml math stack is statically linked into it โ€” folded inward, so the numerical kernels become part of the one library file rather than a constellation of sibling libraries that have to travel alongside it and find each other at load time. That single choice erases most of the packaging problem before it starts: there is almost nothing extra to bundle, because the thing that would need bundling is already inside.

What is left is trust, not linkage. macOS will not run a downloaded app unless it can prove where the app came from, so the Mac build is a Developer-ID-signed, notarized, stapled double-click app โ€” a real .dmg that installs a real .app, optionally wrapped in a lightweight desktop shell that supervises the bundled daemon and gives it a window. It ships the signing binary the daemon needs to authorize actions, and a first-run helper that clears the Gatekeeper quarantine flag so the app opens cleanly the first time instead of being held at the door. We wrote about that trust handshake in a Mac that trusts the app before it opens, and about the double-click app itself in NAOMS is now an app you double-click.

Linux: walk the whole closure, then prove it resolves

Linux is where the same library refuses the same trick. The Linux linker does not allow the static-dedupe fold that macOS uses for this stack, so ggml stays as it is born: a family of separate shared libraries. The language-model library depends on a ggml core library, which depends in turn on the CPU kernels and the base ggml library โ€” a small tree of dependencies. And here is the sharp edge: those shared libraries carry no embedded search path. There is no RUNPATH baked into them telling the loader where their siblings live. Drop them next to each other and, at load time, they still cannot find one another. A naive bundle that copies "the library" copies only the top of the tree and crashes the moment a deeper one is needed.

So the Linux build does the honest, exhaustive thing. It reads the library's declared dependencies and walks them breadth-first โ€” the language-model library to its ggml core, the core to the CPU and base ggml libraries โ€” staging the whole shared-library closure into the bundle's binary directory. Then it does the part we are proudest of: it re-verifies. It re-runs the loader against the staged bundle, with that directory on the library search path, and asks whether every dependency now resolves. If anything is still unresolved โ€” one library the walk missed, one sibling left behind โ€” the build fails. It does not ship a tarball that will crash on a stranger's machine and call that success. This is the Wholeness axiom made mechanical: a thing that claims to be self-contained has to prove it, or it does not ship.

The output is a self-contained tarball: the runtime, the native library and its whole closure, the signing engine, an install script, and a desktop launcher entry. Unpack it, run the installer, and you have the daemon running locally.

We want to be precise about what that is, because it is easy to oversell. Linux ships a self-contained daemon tarball โ€” not a double-click desktop app. The desktop shell that would give Linux the same windowed, supervised experience Mac has lives on an unmerged branch; it is not in the shipped build. So the Linux story today is: a real, self-contained, on-device daemon you install and run โ€” and not, yet, the notarized-app polish Mac has.

There is a coda worth telling, because it is exactly the kind of bug this whole discipline exists to catch. Even after the closure was staged correctly for the app, the production daemon was dying at startup โ€” exiting immediately. The cause was the same no-embedded-search-path property, one level over: one ggml library was not being inherited by the daemon's child process the way the others were, and with no search path baked in, the loader simply could not find it. The fix is a small thing with a load-bearing reason. We seed the library search path into the child process's environment at the moment we spawn it โ€” not in the running process afterward โ€” because the operating system's loader captures that path once, at process start. Set it a moment too late and the loader has already looked, and already given up.

Android: bring the runtime, and mind the page size

Android is a different machine again. The native core is cross-compiled for 64-bit ARM, and โ€” because the build tool will not do this for you โ€” the build bundles the matching C++ runtime shared library from the very same toolchain that compiled the core. Leave it out and the app crashes at startup, because the code is there but the runtime it was compiled against is not. The result is a signed app package with the on-device daemon embedded inside it; your whole memory system, running on your phone, which we celebrated in your whole memory system in your pocket.

The newest wrinkle is physical. Modern Android hardware โ€” newer devices, some Pixels, GrapheneOS โ€” has begun moving from 4 KB memory pages to 16 KB pages, and a 16 KB-page device flatly refuses to load a native library whose loadable segments are aligned to the old 4 KB boundary. So the build now aligns the native libraries to 16 KB, and stores them in the app package uncompressed and page-aligned so the system can map them directly. That work landed this window โ€” the app runs on 16 KB-page devices where before it would not have loaded at all.

Which is the honest frame for Android: it is shipped and running, and it is under an active hardening effort โ€” a live bug epic, not a celebrated 1.0. The 16 KB work is one of the things that epic exists to do.

One pipeline, three platforms, three real daemons โ€” and one honest caveat

The true claim is this: one command family, one shared core, and on Mac, Linux, and Android a self-contained daemon that runs entirely on your own device. That is real on all three today.

The claim we will not make is that it is the same polished app on all three. Only the Mac build is celebrated, and its sign-off narrows the celebration to macOS on purpose โ€” it is the furthest along, the only one with notarized double-click polish. Linux ships a self-contained daemon tarball, and its desktop shell is still on a branch. Android is shipped and running, and still being hardened. The form differs by maturity, even though the core is the same library underneath. Saying so is not a hedge. It is the same discipline that makes the Linux build refuse to ship until every dependency resolves: we would rather tell you exactly what each platform is than let a tidier sentence imply a polish that only one of the three has earned.

Written by AI agents from real project logs; owned and edited by Mujo.


Written by AI agents from real project logs; owned and edited by Mujo.

โ† more in Technology   home โœฆ   all โ†’