# 🤖 memoria

The Oficina suite's memory. One instance per team.

Ask a question, get an answer grounded in your team's own documents —
with a citation you can open, always.

**What exists today is a search engine, not an assistant**, and it has
exactly one model call in it, **switched off unless you switch it on**.
With it off — the default — the trust claim is as strong as it gets:

> memoria's passages and its index are sealed at rest under this team's
> own key, and **no document ever leaves this instance** — not at query
> time, and not at indexing time, because nothing is sent anywhere to be
> indexed.

**With `MEMORIA_AI=on`, one thing changes, and this is all of it.** When
a search asks for a postal address — "office address", "postal address" —
memoria first looks for one with its own checks. Addresses have no
checksum, so those checks cannot find one. It then sends **up to eight
passages per search request** — the top eight that search returned,
passages you were already allowed to read — to **TensorX**, the one named
provider, which points at any addresses in them. **That includes the live
searches that run while you type**, not only the one you submit: each
pause in typing a query that asks for an address is its own request.
memoria shows only text it finds byte-for-byte in those passages, marks
each such row 🤖 with TensorX and the model's name, and never shows the
model's own words. Nothing else is sent: not
the rest of your documents, not your query's other results, and nothing
at all for phone numbers, eircodes, IBANs, VAT numbers or email
addresses, which memoria checks itself. TensorX can read what it
receives; we rely on its retention terms and cannot verify them.

An earlier draft of the first sentence said "nothing leaves this
instance", which was false and a code review caught it. Signing in fetches
`https://<your address's domain>/.well-known/keymail` **before** anyone is
admitted, so an anonymous visitor can make the instance dial any domain
they type. No document leaves; a DNS name does.

**The full assistant changes this again, by much more**, and the design
spec is explicit about how: embeddings come from a hosted API, so
switching it on will send **every document in the corpus** to the named
provider once, at indexing time — not just the passages behind an answer.
That is not built. When it is, this paragraph is replaced by §3.6 of the
design, and not before: §3.6 describes egress this binary does not yet
perform, and saying it early would overstate what leaves as surely as
the old sentence here understated it.

## Live

**https://memoria.oficina.works** — account `bds`, channel `edge`,
serving `71bebef`. Sign in with `paul@keymail.dev`, `paul@rslw.com` or
`paul@tito.io`; real email goes out through the platform's SES, so no
sign-in link is ever written to a log.

It starts **empty**. Paste or upload on **Add content** (`/add`, linked
from the front page) to fill it — the
seed tool needs filesystem access to the database, so it only works
against a local instance.

The origin moved here from `memoria.bds.oncarlos.com` on 2026-09-04, once
the certificate issued — the move `docs`, `sheets` and `calendar` had
already made, along with `id.oficina.works` and the apex. It was
`MEMORIA_ORIGIN` and a restart and nothing else: no hostname is compiled
into this binary.

`memoria.bds.oncarlos.com` still answers. It is the platform route the
custom hostname sits in front of, not a second deployment. Signing in
there still *starts* — but it does not leave you signed in there, because
`MEMORIA_ORIGIN` is the base of every emailed link, so the link lands you
on `oficina.works` and the session cookie is set for that host. Use the
canonical name.

That value is also the CSRF yardstick and the OAuth `client_id` keymail
validates redirects against, so it must match the host the browser is
actually on. Be precise about how much the CSRF half is doing, though:
`csrf.SameOrigin` reads `Sec-Fetch-Site` **before** it compares origins,
so a browser sitting on the old hostname sends `same-origin` and passes.
What refuses is a scripted post carrying a stale `Origin` header. The
mailed link, not the CSRF check, is what actually funnels people onto one
host.

**The DEK is the whole corpus.** It lives in `carlos secrets` for this
app, and a copy is at `memoriadrive/DEK-production.txt`. Lose both and the
data is unreadable — exactly as with `SHEETS_DEK`.

## Trying it locally

```
cd /home/paulca/memoriadrive
./run.sh
```

`run.sh` sets `MEMORIA_ALLOW_LOGGED_MAIL=1`, which is a **local-dev-only**
opt-in: without SMTP configured, rastrillo logs the magic link instead of
mailing it, and that link is a live credential. The app now refuses to
start without either real SMTP or that explicit flag, because the default
had been to log sign-in credentials silently.

Then open **http://127.0.0.1:8823** and sign in. Sign-in goes through
keymail, so use an address on the owners list in `run.sh`
(`paul@tito.io` or `paul@keymail.dev` as it stands).

It comes pre-seeded with 55 documents and 649 passages — every `.md` in
the `oficina` tree, so the first thing you can search is the suite's own
specs. Try `sealed at rest`, `grants join`, `revoke`, `escritorio`,
`plausible empty list`.

Things worth poking at, because each is a decision rather than an
accident:

- Search **ORs** its terms. Ask it a question in words — "what is the
  worst available failure shape" — and it works. ANDing would return
  nothing, and nothing is indistinguishable from having nothing.
- A no-match page tells you how much *is* indexed, so an absent answer
  never looks like an empty index.
- Every result cites a document and a passage number, and the passage
  anchors when you follow it.

### Adding your own

Paste or upload text on **Add content** (`/add`), or seed a directory:

```
cd /home/paulca/memoriadrive
MEMORIA_DEK="$(cat DEK.txt)" ./seed --db m.db --dir /path/to/notes --source notes
```

The seed tool uses the same store, chunker and sealing as the app. It is
the honest ancestor of slice 3's connectors, and it bypasses nothing —
without the right DEK it refuses to run.

### The DEK

`DEK.txt` in `memoriadrive` is the key everything is sealed with. **Lose
it and the corpus is gone**, exactly as with Sheets' `SHEETS_DEK`. The
app never generates one: a generated fallback would seal this boot's data
under a key the next boot cannot derive, losing the data quietly instead
of refusing loudly.

## Deploying

```
make release
carlos deploy --app memoria --account bds --channel edge --timeout 5m releases/memoria-linux-arm64
```

Trust the command `make release` prints over the one written here: it
names the path it actually built. This section used to say
`./releases/memoria-arm64`, which no build has ever produced — the binary
is named for its target, `linux-arm64`.

`carlos deploy` refuses to run when the tree is unclean, and it counts
untracked files. A stray editor directory or dotfile in the checkout is
enough to stop it, with a message about the version defaulting to the git
sha. When the tracked tree is genuinely clean, pass the sha `make release`
stamped — `--version $(git rev-parse --short HEAD)` — rather than
committing noise to get past it.

Flags go **before** the binary path; Go's flag package stops parsing at
the first non-flag argument, so a path first makes `--account` invisible.
`carlos deploy`, never `carlos ship` on its own. Then poll
`GET /api/version` rather than trusting `X-Carlos-Version`, which is the
router's adopted release and flips before the process cycles.

## The design

`docs/superpowers/specs/2026-09-03-memoria-design.md`. Draft 3, which
answers independent reviews of drafts 1 and 2 — four passes across Fable
and GPT Sol, every one returning REQUEST CHANGES. §1.2 records what changed and why — the
central correction being that authorization is asked of whichever app
owns a document, per person, per query, rather than derived from a
membership table memoria keeps.

`QUESTIONS-FOR-PAUL.md` is what I could not decide alone.

## What is not here

No assistant, no embeddings, no sibling apps, no external connectors, no
snapshot of the index (it rebuilds at boot — milliseconds at this size,
and §12 carries the trigger for changing that). The sequencing is §11.
