docs(adr): a private instance's approved sign-ups and the assistant its operator grants, proposed (ADR 0021)

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 14:47:04 +02:00
commit 2d51a77f2c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -0,0 +1,96 @@
# 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
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).
- **Changes:** the rule of 10 Oct 2026 that Halkyon calls no language model with an API
key, for an instance whose operator opts in.
## Context
The developer may stop the public instance and run halkyon.li for themselves and their
close family instead. They don't know in advance which addresses their family will sign in
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.
Two protocols reach nearly every provider: Anthropic's Messages API (Claude), and the
OpenAI-compatible chat completions (OpenAI, Mistral, Google Gemini's compatibility
endpoint, OpenRouter, local models through Ollama, llama.cpp or vLLM). Proton announced a
Lumo API for autumn or winter 2026 without publishing its format; community projects
(lumo-tamer, an unofficial tracker) show an OpenAI-compatible interface.
## Decisions (23 Oct 2026, the developer's)
- **D1 Sign-ups wait for the operator's approval** (over invitation links, which were
recommended, and a sign-up window): anyone can sign up; nothing works until the operator
approves them.
- **D2 The assistant runs on the instance's key, granted per person** (recommended, over
each person's own key and both): the operator pays and switches it on for whom they
choose.
- **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.
## Proposal
1. **Who signs up, per instance:** `SIGN_UP`: `open` (as today), `approval`, or
`closed`. On halkyon.li `approval`.
2. **The operator** is a role on the register. The first one: `operatorSetupCode` in the
passwords; the sign-up that gives it becomes the operator, approved at once, and the
code counts once. A rule by handle would let a stranger who signs up first take the
operator's handle. The operator can name others.
3. **Waiting accounts** (D1): after signing up (SimpleLogin, handle, keys: ADR 0020), an
account waits. Its sessions can only see that they wait, or delete the account;
nothing syncs and nothing uploads. The operator sees who waits in the app (the handle
and when they signed up, which is all the instance knows) and approves or refuses; a
refusal deletes the account. Unapproved after 14 days, it is deleted. At most 10
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,
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
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
(`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
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
person sees their own.
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.
## 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.
- 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.