deep dive

A question becomes a program

millfolio lets a powerful remote model help with your private data without ever seeing it. The trick — the Enclave harness — is to treat the frontier model as an untrusted code generator, not a data processor. Here's how one question turns into code that runs on your Mac.

Two asymmetric models

There are two models, deliberately asymmetric:

What crosses the boundary to the cloud is deliberately thin: the aliased file manifest (file_0, col_2) and the category tag names (phone, travel…) — never your files, your data, or your keyword rules. The frontier-written program then runs as a separate, sandboxed process that can reach the network only over loopback — so inference is a local service it talks to over HTTP, not an in-process call. That's why there are two servers (and why they stay two: the app restarts without reloading the ~7 GB model). Both run under launchd.

The contract the frontier model is given is a single document — the enclave system prompt — loaded at runtime. It spells out the confidentiality rules, the current Mojo dialect, and the tool surface (search, csv_rows, pdf_text, docx_text, ask_local, print_answer, …).

Example: a question becomes a program

You ask "How much did I spend on travel last year?". The frontier model — seeing only aliases and the tag names — writes this, and the Enclave compiles it and runs it in a sandbox that can reach only your local model:

# written by the frontier model — it never sees a single real value
from vault import *
def main() raises:
    var hits = search("travel transportation flights hotels expenses", 40)
    var total = 0.0
    for c in hits:
        # ask_local reads the REAL chunk on-device; returns "amount|yes" or "0|no"
        var verdict = ask_local(
            "If this is a 2025 travel expense, reply '<amount>|yes', else '0|no'.", c.text)
        var parts = verdict.split("|")
        if len(parts) == 2 and String(parts[1]) == "yes":
            total += atof(String(parts[0]))
    print_answer("You spent about $" + String(total) + " on travel in 2025.")

The frontier model orchestrates over aliases; search and ask_local do the real work locally; the sum is computed on your machine and print_answer surfaces it there. The search results and the answer are never returned to the frontier model — which is exactly why the program model is load-bearing, not an implementation detail.

Why it holds

Containment lives outside the model, at the OS level. The generated program runs under a Seatbelt profile that denies all network except loopback to your local engine — it can't phone home. An egress guard gates every message to the frontier (fails closed), and the compile-feedback loop only ever sends back aliased source, never runtime output that might contain real content. Your documents never leave the Mac, and never reach the frontier model. See the walkthrough to try it, or Enclave for the full design.

Want to see it concretely? Dissect a real run — the exact prompt sent to the frontier model, the Mojo program it wrote, and the raw sentinel-delimited output that program printed, for one question over the synthetic demo vault. Or explore the whole system as an interactive architecture diagram ↗.

Categories, by rule — not by guesswork

"How much did I spend on my phone bill?" used to mean asking the local model, transaction by transaction, "is this a phone carrier?" — slow, and occasionally wrong in spectacular ways (a stray account number once read as a phone number turned a phone bill into $224,303). Now categories are deterministic rules, matched once when your statements are indexed — so a category answer is a fast, exact filter over stored tags, with no per-transaction model call.

Categories are rules, not guesses. A deterministic registry — the built-ins (phone, travel, restaurant, groceries, health) plus anything you add to categories.txt — tags every transaction once at index time, with no model call. So a category answer is a fast, exact filter over stored tags. The frontier model is told only the tag names (the vocabulary it can filter on) — never your keyword rules, which hold the merchant strings you typed and never leave the Mac.

A registry you can edit

The registry ships with sensible built-ins — phone, travel, restaurant, groceries, health — and is just a text file you own at ~/.config/millfolio/categories.txt. Add your own categories, or extend a built-in, then re-index:

# ~/.config/millfolio/categories.txt — <tag> = keyword, keyword, …
pets = chewy, petco, the vet
subscriptions = netflix, spotify, hbo max
phone = my local carrier        # extends the built-in phone tag

Matching is case-insensitive and multi-valued — an airport restaurant can be both travel and restaurant. And because the rules are deterministic, a credit-card payment with a long account number is never mistaken for a phone bill: it matches no carrier keyword, so it gets no tag.

What the model sees — names, not rules

At query time the frontier model is handed the available tag names — the vocabulary it's allowed to filter on, including your custom ones — and writes a program over the already-tagged transactions:

from vault import *
def main() raises:
    var total = 0.0
    for f in manifest():
        for t in transactions(f.alias):       # exact, reconciled, already tagged
            if t.direction == "debit" and "phone" in t.tags:
                total += t.amount
    print_answer("You paid " + money(total) + " on your phone bill.")

The keyword rules — which hold the merchant strings you typed — stay on your Mac; only the tag names ever cross to the cloud, and the egress guard gates that message like every other. Anything the rules don't name still falls back to the on-device ask_local path shown above.

AI rules — the fuzzy tail, materialized once

Some categories resist keywords. "Is this a gym?" can't be spelled as a merchant list, so a category can instead be an AI rule — a plain-English yes/no question the on-device model answers per transaction. Write it with a : instead of an =:

# an AI rule — <tag> : <question>, answered on-device
gym : is this a gym or fitness membership?

A model call per transaction is far too slow to run at query time, so an AI rule is materialized once and cached into the same .tags column the keyword rules write — after which "how much on the gym" is the identical fast, exact filter. The tricky part is knowing what's already been computed: millfolio keeps a tiny per-rule completion marker keyed on a monotonic insertion generation — so a back-dated statement you import months later is correctly seen as new work, and a true negative ("not a gym") is classified exactly once, never again. Add a rule, or a new statement, and only the genuinely-new transactions are ever re-run.

It's incremental and observable: a materialization panel in the Tags tab shows a per-rule progress bar, a ready/pending badge, and a Materialize now button, and a between-questions worker chips away in the background (pausable). Until a rule is fully materialized it's held back from the model's tag vocabulary — so a half-computed gym filter can never quietly report "$0 on the gym". The verdicts are computed by the local model; nothing about them leaves the Mac.