Skip to content

wisp 0.0.1

Choose a tag to compare

@vcealicu vcealicu released this 02 Oct 16:06
· 19 commits to main since this release

LocalGhost wisp 0.0.1

The first release cut. 2 October 2026.

A wisp is the smallest ghost there is, which is what this is: the first build of the box that a
phone can take a release of, and the first one with a name. Everything below is what a box built
from the tag v0.0.1 does, what is in it, and how it fits together. The release is pinned to its
commit (releases/pins.txt), so tools/cut_release.sh 0.0.1 builds the same bytes again from the
same place.

What LocalGhost is

A box at home that keeps a person's photos, trail, notes, voice, health and chats, reads them
with models that run on the box, and gives them back on the phone as memories, days, places and
answers. Nothing of the person's leaves the box. The box asks the internet only for public things
(news feeds, prices, Wikipedia, map data), and only through a signed mirror where it can.

Two parts:

  • the server, a fleet of Go daemons on a Debian box with a GPU, one process per job, all on
    an encrypted volume that opens with a PIN from the phone;
  • the app, Kotlin and Compose on Android, the only client, which talks to the box over mTLS
    with a certificate made at enrolment.

What is in wisp

The vault

  • One encrypted volume (LUKS) holding the person's data, the databases and the daemons' state.
    Its key is sealed to the TPM where the box has one, or to a software seal (Argon2id) where it
    does not.
  • One PIN opens it from the phone. Wrong PINs slow down and then lock out; a wipe PIN destroys
    the key. While the volume is locked the box answers every call as if it were down, so a phone
    taken from someone shows nothing that says a box exists.
  • The unlock runs as six steps (resolve, unseal, mount, start the databases, start the cache,
    start the daemons), which the app draws as six rings closing; the lock plays them backwards.
  • On the volume: Postgres and Redis of its own (never the host's), the daemons' binaries, the
    models, the geo data and the archive.

The daemons (cmd/ghost.*)

  • ghost.secd is the one thing on the system disk: the front door (HTTPS with client
    certificates), the unlock, the sessions, the API the phone speaks, and the parent of watchd.
  • ghost.watchd starts and watches the cohort on the volume and restarts what falls over.
  • ghost.framed takes photos and videos from the phone, archives the originals, makes previews,
    reads EXIF and video metadata for time and place, geocodes against the box's own GeoNames,
    tags and captions through the model, finds damaged files and moves them aside, draws each day's
    trail as a path and tells it as stays and moves along the streets, cuts the coastline and road
    tiles the map uses, rasterises the time zones, and reads the heights under the trail.
  • ghost.noted takes text (notes, the evening check-in, chats) into the journal.
  • ghost.voiced takes voice notes, transcribes them on the box (whisper.cpp) and journals them.
  • ghost.tallyd takes the health sync (Health Connect days and samples), the prices from the
    exchanges every minute and every five seconds, the ECB table, the rank list, the daily candles
    and the price history, and keeps CRYPTO50.
  • ghost.synthd is the memory layer: it distills the journal into memories, writes the about-me
    note and the check-ins into facts about the person and their people, counts the places, notices
    things once a day, writes each day's story, groups the news into stories and writes the brief,
    writes the coin pages, and answers the chat with the person's own context.
  • ghost.cued watches for the moment: a place nearby that suits the person's taste, a
    reflection on an old memory.
  • ghost.shadowd is the sibling that challenges: it reads what the others wrote and says when
    something looks wrong.
  • ghost.searchd indexes the archive for search (full text and vectors, EmbeddingGemma) and
    runs the captioning queue.
  • ghost.oracled serves the models: llama.cpp with Gemma 4 12B and its vision projector on the
    GPU, the CPU when the GPU is busy, with a thinking channel the chat can open.
  • ghost.restore, ghost-update-guard, ghost-cli, ghost-ctl, ghost-setup and the
    tile cutters are the operator's tools.

The archive

  • Photos and videos from the phone, every original kept as it is, deduplicated by hash.
  • For each: a preview, the time and place it was taken (EXIF, the video's metadata, the
    phone's hint), a place name from the box's own geocoder, a description, a title and tags from
    the model, and the search index.
  • The gallery, a viewer, tag editing, search by place and tag.
  • The archive's progress on Box Status, folded under ghost.framed.

The trail

  • The phone keeps a trail of where it is, sealed to its own hardware, and hands it to the box in
    batches that only the box can open.
  • The box keeps the points and draws each day: the line, the glitches thrown out, the distance,
    the day as stays and moves (on foot along the streets where the box has the roads, by road
    between the fixes), and questions about stretches that look wrong ("were you there?").
  • The map draws the land, the coast at full detail, the roads, the place names and the trail from
    the box's own data: Natural Earth, OpenStreetMap's land polygons and roads, GeoNames. No tile
    server, ever.
  • The heights under the trail from the Copernicus DEM at 90 m, read on the box: each day's climb
    and highest point, the height at any point of the line.
  • The time zone the trail's newest point is in names the person's day, so "today" and "19:00"
    are theirs, not the box's.

The days

  • A story of each day, written by the model from the photos (and what they show), the trail (the
    stays and the moves), the health sync (steps, sleep, exercise), the voice notes and the evening
    check-in (how they felt and why). Kept only when every number in it is one the facts gave.
  • A page for every day on the phone: the story, the photos, the outing it was part of, where it
    went, the body, the notes; the day before and after, the same date a year either side.
  • This day in earlier years, on home and in memories.

Memories

  • Distilled from the chats, the notes and the voice notes: durable facts, preferences, plans
    and events, written with the person's name ("Vlad prefers…") so a search for a name finds them.
  • Me and my people from the about-me note the person writes in the app, and from what they say
    at the check-ins: facts about them and one memory per person ("Cristina is Vlad's partner…").
  • Places counted from the trail and the photos: where the person keeps going, how many days,
    how long, the weekday it is most often, the photos taken there.
  • Noticed once a day: the model reads a sheet of the last weeks (the places, the walking, the
    steps by weekday, the photos and what is in them, the check-ins' feelings, the people mentioned)
    and writes up to three short memories that notice a habit, a change or a contrast, kept only
    when every number is on the sheet.
  • Outings from the photos (a trip is a run of photo days away from home) with their covers,
    and the taste: what the person photographs, as likes by category and interests to match
    places against.
  • Every memory can be edited or deleted on the phone; the person's version wins and the box never
    writes over it.

Home and FOR YOU

  • Home opens on the latest the phone kept: BTC, ETH and SOL with the day's change, CRYPTO50, the
    day's news as one point per story (the brief), and FOR YOU.
  • FOR YOU: places near the trail that suit the person's taste, this day in earlier years, the
    day's news that touches what they said about themselves, and one memory brought back (what the
    box noticed lately, else a different one each day).
  • The notifications check and the trail upload bring home's numbers back with them, so home is
    current without asking.

The chat

  • The box's model, with the person's memories, the matching photos summarised, the search index,
    the news, the prices and the box's own Wikipedia as context, each piece shown to the person as
    what the box used and why.
  • The phone searches the web when asked (DuckDuckGo, or Brave with the person's own key) and
    reads the pages, so the box never does; the findings come with the question.
  • The chat knows its name (LocalGhost), who it is talking to, and what the person wrote about
    themselves and their people.
  • A thinking level (off, brief, deep); the answer keeps being written on the box when the phone
    goes away.

News and prices

  • Feeds fetched by the phone on Wi-Fi (or by the box when the phone is away), grouped into
    stories across outlets, each told as a lead and points from the articles' own text where the
    page serves it; the day's brief; two digests a day.
  • The top hundred coins priced every minute by the box's own blend of every market it follows
    (Binance, Kraken, Bitfinex, Coinbase, Bitstamp, OKX, Gemini), through USD, USDT, USDC, EUR, BTC
    and ETH; BTC, ETH and SOL every five seconds; CRYPTO50, the fifty largest weighted by volume; the
    ECB's table; daily candles and history walked back through the years.
  • A page per coin: the price rolling, the chart over a day to all time, the figures, what the coin
    is (read up by the box from its own Wikipedia, the coin's site and Coinbase, and written by the
    model), and where the price comes from, market by market.
  • Box Status shows how every feed is doing, folded under ghost.tallyd.

Health, voice, phrases

  • Health Connect days and samples (steps, sleep, exercise, heart rate, active calories) synced to
    the box, shown on HEALTH, and in the day stories.
  • Voice notes recorded on the phone, transcribed on the box, journaled, played back.
  • The lock-screen card with the day's news and the prices; phrase packs offered when the phone
    arrives in a new country.

Notifications

  • Every daemon speaks through one store: the check-in reminder, a day a year ago, something new
    nearby, the news digests, a service that went down, the week's highlight. A week is kept; each
    opens what it is about (the day, the memory, the news, Box Status).
  • Nothing goes through a third party: the phone polls the box.

Releases from the phone

  • The phone reads the mirror's manifest once a day on Wi-Fi and says when a newer release is
    there ("wisp 0.0.1 is out").
  • DEPLOY downloads the set on the phone and hands it to the box; the box checks the manifest's
    signature with the site key it already holds and every file's hash, unpacks the release, keeps
    the previous build, restarts onto the new one and opens a trial.
  • The release rolls back by itself if the first unlock onto it fails, if a critical daemon keeps
    falling over in the first ten minutes, if the box keeps restarting, or when the person taps
    ROLL BACK.

How it all works

The shape

The phone talks only to ghost.secd, over HTTPS with a client certificate the box issued at
enrolment (the box is its own CA). secd holds no data of its own; it opens the volume with the
PIN, starts the databases and watchd, and from then on reads and writes the volume's Postgres and
Redis on the phone's behalf, and hands uploads to the daemons' inboxes as files. Each daemon has a
control socket (ghost-cli ghost.<name> <command>) and a health endpoint; watchd restarts what
dies and Box Status shows all of it.

The data

One Postgres on the volume (internal/hw/schemadef.go is the schema, converged at every start;
dropping anything is a named, dated migration, never automatic). Redis holds the hot copies the
phone reads first (the prices, the news, home). The archive's originals and previews, the trail's
day files, the voice notes and the geo data are files on the volume beside the databases.

The models

llama.cpp built from the mirror's pinned source, serving Gemma 4 12B (Q4_K_M) with its vision
projector on the GPU through ghost.oracled; EmbeddingGemma for the search index through
ghost.searchd; whisper.cpp for speech through ghost.voiced. Every pass that needs the model asks
whether the GPU is free and waits otherwise. What the model writes is checked against what it
was given: a number not in the facts, a refusal, a list where prose was asked for, and the text
is dropped rather than kept.

The mirror

Setup and updates take public files only from www.localghost.ai/mirror: a manifest of SHA-256s
signed by the site key, whose fingerprint is pinned in the repo. A file is named only after its
hash matches; an unreachable mirror stops the step; nothing falls back to an upstream. The sets:
geo (GeoNames, Natural Earth), landpolygons and roads (OpenStreetMap), tz, elevation (Copernicus),
wikipedia (Kiwix's ZIM), models, embeddings, llama, whisper, speech, go, phone, and server (the
releases themselves). tools/update.sh brings a running box current with any of them.

What leaves the box

Public requests only: the exchanges and the ECB for prices (by the box), feeds and web pages
(by the phone, mostly), the mirror for data and releases, Wikipedia's API only on a box without
the local copy, a coin's own website for its page. Never a map tile, never a location, never a
photo, never a note. The phone's web search is the phone's.

The build

make box builds every daemon with the tree's git description as its version. A release is cut
with tools/cut_release.sh <version> from the tag v<version> in a clean worktree, through
tools/release_build.sh: CGO off, -trimpath, no build id, the tar sorted with the commit's time,
gzip -n, so the same tag gives the same bytes anywhere. The release carries its name from
tools/release.names and these notes.

What the release holds

One GitHub release, v0.0.1, with everything:

  • server/: the server set, what a box takes from the phone and the mirror carries: the bundle
    (localghost-server-0.0.1-linux-amd64.tar.gz, built reproducibly from the tag), RELEASE.txt,
    these notes as NOTES.md, NOTICE.txt, TERMS-MIT.txt.
  • app/: the Android app, localghost-app-0.0.1.apk, built at the same commit and signed with
    the app's keystore, with its GPG signature by the site key and APP.txt (its hash and commit).
  • source/: localghost-0.0.1-source.tar.gz, the whole tree at the tag.
  • SHA256SUMS over all of it, signed by the site key (SHA256SUMS.asc, info@localghost.ai, the
    key at https://www.localghost.ai/.well-known/pgp-key.asc).

Installing wisp

A box

From the source at the tag (the archive in the release, or git clone and git checkout v0.0.1),
on a Debian box with a GPU, server/tools/README.md is the whole of it, step by step. In short:
tools/server_setup_root.sh (the system), the service user's build (make box), the app on the
phone, ghost.secd setup dry run and --apply (the PINs, the volume, the CA, the units; it
renders the QR), scan the QR with the app (scanning is the enrolment), the first unlock, then
tools/setup_llama.sh (llama.cpp and the models from the mirror) and tools/fetch_geo.sh (the
maps). Everything setup downloads comes from the mirror, checked against its signed manifest.
Later, sudo ./tools/update.sh brings a running box current with the mirror's sets (the heights
with GHOST_GEO_ELEVATION, Wikipedia with update.sh wiki). From the next release on, a box
takes the server from the phone (SETTINGS › SERVER › DEPLOY); the setup is once.

The phone

Install localghost-app-0.0.1.apk on an Android phone (Android 15 or later): download it, open
it, allow the install from this source. Then scan the box's QR (the setup prints it; ghost-qr
mints a fresh one any time): that is the enrolment, and the main PIN unlocks. The app's VERIFY
BUILD screen shows the commit it was built from, the source manifest's root and the signing
certificate, to check against this release; app/android/VERIFY.md says how.

Known gaps at wisp

  • The box CA key sits on the OS disk, and secd trusts the client-certificate header on the
    loopback listener; both are on the security list.
  • Releases are signed by the site key alone; an offline release key is planned.
  • Mail, the Mist (peer-to-peer backup) and the decoy volume are not built.
  • xyntai, the first box, still runs the software seal; ghost-ctl migrate-to-tpm moves it.