docs(adr): no hand-off and no connector (ADR 0022); several providers and personal keys in ADR 0021

ADR 0022, decided 23 Oct: the hand-off to assistants outside the app and
the Claude connector (ADR 0006) go in v0.19.0, app and server together;
the field keeps the web search. ADR 0021 takes the developer's D4-D6: the
operator's keys for several providers, granted per person; personal keys
on the person's devices, only where the operator allows them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-23 15:08:40 +02:00
commit 1454fd7bfc
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,9 +1,9 @@
# ADR 0021: A private instance: approved sign-ups and an assistant the operator grants
- **Status:** proposed 23 Oct 2026. Decided by the developer on 23 Oct: D1–D3 below; the
- **Status:** proposed 23 Oct 2026. Decided by the developer on 23 Oct: D1–D6 below; the
rest under *Proposal* is for review before building.
- **Builds on:** ADR 0006 (the assistant connector), ADR 0016 (the register), ADR 0019
(private studysets), ADR 0020 (SimpleLogin).
- **Builds on:** ADR 0016 (the register), ADR 0019 (private studysets), ADR 0020
(SimpleLogin), ADR 0022 (the hand-off and the connector gone).
- **Changes:** the rule of 10 Oct 2026 that Halkyon calls no language model with an API
key, for an instance whose operator opts in.
@ -16,8 +16,14 @@ with, and with SimpleLogin (ADR 0020) the instance keeps no addresses anyway.
On 10 Oct the developer ruled out API keys for language models: on a public instance anyone
could drive up the bill, and each person should decide what a model sees. Assistants came
through people's own subscriptions instead: the connector (ADR 0006) and the hand-off to
Claude or Lumo. A family instance whose operator approves every account takes away the
first reason; the second holds if the app shows what it sends.
Claude or Lumo, both gone since 23 Oct (ADR 0022). A family instance whose operator
approves every account takes away the first reason; the second holds if the app shows
what it sends.
People in one family may prefer different providers (the developer: "I might use myself
the Claude API while other might want to use the OpenAI API, then one other the Proton
Lumo API"). A plan for Claude's or ChatGPT's apps is no API access: the APIs are billed
apart, by their providers' consoles.
Two protocols reach nearly every provider: Anthropic's Messages API (Claude), and the
OpenAI-compatible chat completions (OpenAI, Mistral, Google Gemini's compatibility
@ -36,6 +42,14 @@ Lumo API for autumn or winter 2026 without publishing its format; community proj
- **D3 Individual limits and statistics** (the developer: "so I can see after the fact who
potentially drove up the bill"): each person's use is limited and counted, and the
operator sees it per person.
- **D4 Several providers, all on the operator's keys** (over personal keys alone, and over
both, which was recommended): the operator sets up the assistants the instance offers
and grants each person one or more.
- **D5 Personal keys stay on the person's devices** (recommended, over keeping them on the
instance): the app calls the provider itself, and the instance sees neither key nor
text.
- **D6 Personal keys only where the operator allows them** (over every instance, which was
recommended): an instance setting, off unless switched on.
## Proposal
@ -53,29 +67,35 @@ Lumo API for autumn or winter 2026 without publishing its format; community proj
waiting at once, and sign-ups per network limited as today's, so strangers can't flood
the list. The operator's app shows a badge; there is no mail, as the instance keeps no
addresses.
4. **The provider**, set by the operator: `ASSISTANT_PROVIDER` (`anthropic`, or `openai`
for every OpenAI-compatible service), `ASSISTANT_BASE_URL` (each provider's own
unless set), `ASSISTANT_MODEL`, and the key `assistantApiKey` in the passwords.
Unset: no assistant on the instance, as today. Switching from Claude to OpenAI or to
Lumo is a change of these and a restart; no app changes.
5. **Two adapters of our own** behind one interface in the server, over plain HTTP, no
SDKs. Features use only what both protocols share: a system prompt and messages,
4. **The assistants**, set by the operator (D4): one or more, each with an ID, the name
people see ("Claude Sonnet"), its protocol (`anthropic`, or `openai` for every
OpenAI-compatible service), its base URL (the provider's own unless set) and model in
`deploy/.env` (`ASSISTANTS=claude,gpt`, then `ASSISTANT_CLAUDE_NAME`, `_PROTOCOL`,
`_BASE_URL`, `_MODEL`), and its key in the passwords (`ASSISTANT_CLAUDE_KEY`). There
rather than in the app: the keys stay out of the database and its hourly backups, where
the operator keeps the instance's other secrets. None set: no assistant on the
instance, as today. Adding Lumo once its API is out, or switching a model, is a change
of these and a restart; no app changes.
5. **Two adapters of our own** behind one interface, over plain HTTP, no SDKs, in a pure
Dart package of their own, which the server uses for the operator's assistants and the
app for personal keys (10). Features use only what both protocols share: a system prompt and messages,
streamed text, and tool calls where a feature needs them. Tested against recorded
replies of each. Not chosen: LiteLLM as a sidecar (a Python proxy for about 100
providers: one more container to run and update, for providers never used) and
langchain_dart (a large framework for a small job).
6. **Grants** (D2): nobody has the assistant until the operator switches it on for them,
with a limit. The person agrees once to what goes where: the provider's name, that the
6. **Grants** (D2, D4): nobody has an assistant until the operator grants it to them, one
assistant at a time, each grant with its limits. The person agrees once to what goes where: the provider's name, that the
instance sees the text on its way, and that the operator sees how much they used,
never what.
7. **Asking:** the app shows what it will send, the person sends it to the instance
7. **Asking:** the field offers the assistants granted beside the web search (the one last
used first), the app shows what it will send, the person sends it to the instance
(`assistant.ask`, the answer streamed), the instance checks the grant and what is left
of the limit, forwards the request, and counts what the provider's reply reports
(input and output tokens). Past the limit it refuses (`limitReached`), saying when it
resets. A private studyset (ADR 0019) goes only as far as the person sends it,
decrypted on the device for that request.
8. **Limits and statistics** (D3): per person, per day and per month (a day's limit stops a
runaway loop early). Counted per person, day and model: requests, input and output
8. **Limits and statistics** (D3): per grant, per day and per month (a day's limit stops a
runaway loop early). Counted per person, day and assistant: requests, input and output
tokens, and the cost at the prices the operator enters per model (per million tokens;
without prices, tokens only, and limits in tokens). Neither prompts nor answers are
kept. The operator sees everyone's figures in the app, per person and month; each
@ -83,14 +103,23 @@ Lumo API for autumn or winter 2026 without publishing its format; community proj
9. **The privacy page** names the provider, says what is sent and that the instance sees
it in passing, that the operator sees each person's figures, and that no content is
kept.
10. **The connector and the hand-off stay** (ADR 0006): anyone may still use their own
Claude or Lumo.
10. **Personal keys** (D5, D6), where the operator switches them on (`ASSISTANT_OWN_KEYS`,
off unless `on`; `instance.info()` says so, and the app offers them only then): a
person adds a provider in Settings (protocol, base URL, model, their key), and may add
several. The key goes into their settings, which are sealed with their personal key
(ADR 0019), so it reaches their other devices and the instance can't read it. The app
calls the provider directly with the same adapters (5); the instance sees neither key
nor text and counts nothing, and the person's limits are their provider's. The
instance can't stop a modified app from doing so, and needn't: nothing passes through
it. Personal assistants are offered in the field beside those granted.
## Consequences
- The operator pays the provider, and sees who used how much, never what for.
- The instance sees what is asked on its way to the provider; the provider's own terms
then apply to it.
- A provider that is down stops the in-app assistant, not the hand-off or the connector.
- A provider that is down stops the assistants on it; the others granted, personal keys
and the web search go on.
- Strangers can sign up and wait, but can't use anything, and their accounts lapse.
- Public instances keep the rule of 10 Oct: their operators leave the assistant unset.
- Public instances keep the rule of 10 Oct: their operators leave the assistants unset,
and may allow personal keys.