| 1 | # π€ memoria |
| 2 | |
| 3 | The Oficina suite's memory. One instance per team. |
| 4 | |
| 5 | Ask a question, get an answer grounded in your team's own documents β |
| 6 | with a citation you can open, always. |
| 7 | |
| 8 | **What exists today is a search engine, not an assistant**, and it has |
| 9 | exactly one model call in it, **switched off unless you switch it on**. |
| 10 | With it off β the default β the trust claim is as strong as it gets: |
| 11 | |
| 12 | > memoria's passages and its index are sealed at rest under this team's |
| 13 | > own key, and **no document ever leaves this instance** β not at query |
| 14 | > time, and not at indexing time, because nothing is sent anywhere to be |
| 15 | > indexed. |
| 16 | |
| 17 | **With `MEMORIA_AI=on`, one thing changes, and this is all of it.** When |
| 18 | a search asks for a postal address β "office address", "postal address" β |
| 19 | memoria first looks for one with its own checks. Addresses have no |
| 20 | checksum, so those checks cannot find one. It then sends **up to eight |
| 21 | passages per search request** β the top eight that search returned, |
| 22 | passages you were already allowed to read β to **TensorX**, the one named |
| 23 | provider, which points at any addresses in them. **That includes the live |
| 24 | searches that run while you type**, not only the one you submit: each |
| 25 | pause in typing a query that asks for an address is its own request. |
| 26 | memoria shows only text it finds byte-for-byte in those passages, marks |
| 27 | each such row π€ with TensorX and the model's name, and never shows the |
| 28 | model's own words. Nothing else is sent: not |
| 29 | the rest of your documents, not your query's other results, and nothing |
| 30 | at all for phone numbers, eircodes, IBANs, VAT numbers or email |
| 31 | addresses, which memoria checks itself. TensorX can read what it |
| 32 | receives; we rely on its retention terms and cannot verify them. |
| 33 | |
| 34 | An earlier draft of the first sentence said "nothing leaves this |
| 35 | instance", which was false and a code review caught it. Signing in fetches |
| 36 | `https://<your address's domain>/.well-known/keymail` **before** anyone is |
| 37 | admitted, so an anonymous visitor can make the instance dial any domain |
| 38 | they type. No document leaves; a DNS name does. |
| 39 | |
| 40 | **The full assistant changes this again, by much more**, and the design |
| 41 | spec is explicit about how: embeddings come from a hosted API, so |
| 42 | switching it on will send **every document in the corpus** to the named |
| 43 | provider once, at indexing time β not just the passages behind an answer. |
| 44 | That is not built. When it is, this paragraph is replaced by Β§3.6 of the |
| 45 | design, and not before: Β§3.6 describes egress this binary does not yet |
| 46 | perform, and saying it early would overstate what leaves as surely as |
| 47 | the old sentence here understated it. |
| 48 | |
| 49 | ## Live |
| 50 | |
| 51 | **https://memoria.oficina.works** β account `bds`, channel `edge`, |
| 52 | serving `71bebef`. Sign in with `paul@keymail.dev`, `paul@rslw.com` or |
| 53 | `paul@tito.io`; real email goes out through the platform's SES, so no |
| 54 | sign-in link is ever written to a log. |
| 55 | |
| 56 | It starts **empty**. Paste or upload on **Add content** (`/add`, linked |
| 57 | from the front page) to fill it β the |
| 58 | seed tool needs filesystem access to the database, so it only works |
| 59 | against a local instance. |
| 60 | |
| 61 | The origin moved here from `memoria.bds.oncarlos.com` on 2026-09-04, once |
| 62 | the certificate issued β the move `docs`, `sheets` and `calendar` had |
| 63 | already made, along with `id.oficina.works` and the apex. It was |
| 64 | `MEMORIA_ORIGIN` and a restart and nothing else: no hostname is compiled |
| 65 | into this binary. |
| 66 | |
| 67 | `memoria.bds.oncarlos.com` still answers. It is the platform route the |
| 68 | custom hostname sits in front of, not a second deployment. Signing in |
| 69 | there still *starts* β but it does not leave you signed in there, because |
| 70 | `MEMORIA_ORIGIN` is the base of every emailed link, so the link lands you |
| 71 | on `oficina.works` and the session cookie is set for that host. Use the |
| 72 | canonical name. |
| 73 | |
| 74 | That value is also the CSRF yardstick and the OAuth `client_id` keymail |
| 75 | validates redirects against, so it must match the host the browser is |
| 76 | actually on. Be precise about how much the CSRF half is doing, though: |
| 77 | `csrf.SameOrigin` reads `Sec-Fetch-Site` **before** it compares origins, |
| 78 | so a browser sitting on the old hostname sends `same-origin` and passes. |
| 79 | What refuses is a scripted post carrying a stale `Origin` header. The |
| 80 | mailed link, not the CSRF check, is what actually funnels people onto one |
| 81 | host. |
| 82 | |
| 83 | **The DEK is the whole corpus.** It lives in `carlos secrets` for this |
| 84 | app, and a copy is at `memoriadrive/DEK-production.txt`. Lose both and the |
| 85 | data is unreadable β exactly as with `SHEETS_DEK`. |
| 86 | |
| 87 | ## Trying it locally |
| 88 | |
| 89 | ``` |
| 90 | cd /home/paulca/memoriadrive |
| 91 | ./run.sh |
| 92 | ``` |
| 93 | |
| 94 | `run.sh` sets `MEMORIA_ALLOW_LOGGED_MAIL=1`, which is a **local-dev-only** |
| 95 | opt-in: without SMTP configured, rastrillo logs the magic link instead of |
| 96 | mailing it, and that link is a live credential. The app now refuses to |
| 97 | start without either real SMTP or that explicit flag, because the default |
| 98 | had been to log sign-in credentials silently. |
| 99 | |
| 100 | Then open **http://127.0.0.1:8823** and sign in. Sign-in goes through |
| 101 | keymail, so use an address on the owners list in `run.sh` |
| 102 | (`paul@tito.io` or `paul@keymail.dev` as it stands). |
| 103 | |
| 104 | It comes pre-seeded with 55 documents and 649 passages β every `.md` in |
| 105 | the `oficina` tree, so the first thing you can search is the suite's own |
| 106 | specs. Try `sealed at rest`, `grants join`, `revoke`, `escritorio`, |
| 107 | `plausible empty list`. |
| 108 | |
| 109 | Things worth poking at, because each is a decision rather than an |
| 110 | accident: |
| 111 | |
| 112 | - Search **ORs** its terms. Ask it a question in words β "what is the |
| 113 | worst available failure shape" β and it works. ANDing would return |
| 114 | nothing, and nothing is indistinguishable from having nothing. |
| 115 | - A no-match page tells you how much *is* indexed, so an absent answer |
| 116 | never looks like an empty index. |
| 117 | - Every result cites a document and a passage number, and the passage |
| 118 | anchors when you follow it. |
| 119 | |
| 120 | ### Adding your own |
| 121 | |
| 122 | Paste or upload text on **Add content** (`/add`), or seed a directory: |
| 123 | |
| 124 | ``` |
| 125 | cd /home/paulca/memoriadrive |
| 126 | MEMORIA_DEK="$(cat DEK.txt)" ./seed --db m.db --dir /path/to/notes --source notes |
| 127 | ``` |
| 128 | |
| 129 | The seed tool uses the same store, chunker and sealing as the app. It is |
| 130 | the honest ancestor of slice 3's connectors, and it bypasses nothing β |
| 131 | without the right DEK it refuses to run. |
| 132 | |
| 133 | ### The DEK |
| 134 | |
| 135 | `DEK.txt` in `memoriadrive` is the key everything is sealed with. **Lose |
| 136 | it and the corpus is gone**, exactly as with Sheets' `SHEETS_DEK`. The |
| 137 | app never generates one: a generated fallback would seal this boot's data |
| 138 | under a key the next boot cannot derive, losing the data quietly instead |
| 139 | of refusing loudly. |
| 140 | |
| 141 | ## Deploying |
| 142 | |
| 143 | ``` |
| 144 | make release |
| 145 | carlos deploy --app memoria --account bds --channel edge --timeout 5m releases/memoria-linux-arm64 |
| 146 | ``` |
| 147 | |
| 148 | Trust the command `make release` prints over the one written here: it |
| 149 | names the path it actually built. This section used to say |
| 150 | `./releases/memoria-arm64`, which no build has ever produced β the binary |
| 151 | is named for its target, `linux-arm64`. |
| 152 | |
| 153 | `carlos deploy` refuses to run when the tree is unclean, and it counts |
| 154 | untracked files. A stray editor directory or dotfile in the checkout is |
| 155 | enough to stop it, with a message about the version defaulting to the git |
| 156 | sha. When the tracked tree is genuinely clean, pass the sha `make release` |
| 157 | stamped β `--version $(git rev-parse --short HEAD)` β rather than |
| 158 | committing noise to get past it. |
| 159 | |
| 160 | Flags go **before** the binary path; Go's flag package stops parsing at |
| 161 | the first non-flag argument, so a path first makes `--account` invisible. |
| 162 | `carlos deploy`, never `carlos ship` on its own. Then poll |
| 163 | `GET /api/version` rather than trusting `X-Carlos-Version`, which is the |
| 164 | router's adopted release and flips before the process cycles. |
| 165 | |
| 166 | ## The design |
| 167 | |
| 168 | `docs/superpowers/specs/2026-09-03-memoria-design.md`. Draft 3, which |
| 169 | answers independent reviews of drafts 1 and 2 β four passes across Fable |
| 170 | and GPT Sol, every one returning REQUEST CHANGES. Β§1.2 records what changed and why β the |
| 171 | central correction being that authorization is asked of whichever app |
| 172 | owns a document, per person, per query, rather than derived from a |
| 173 | membership table memoria keeps. |
| 174 | |
| 175 | `QUESTIONS-FOR-PAUL.md` is what I could not decide alone. |
| 176 | |
| 177 | ## What is not here |
| 178 | |
| 179 | No assistant, no embeddings, no sibling apps, no external connectors, no |
| 180 | snapshot of the index (it rebuilds at boot β milliseconds at this size, |
| 181 | and Β§12 carries the trigger for changing that). The sequencing is Β§11. |
| 182 | |