The developer decided on 23 Oct to drop the hand-off to an AI assistant outside the app, and Claude's connector with it, in v0.19.0, the app and the server together: build 22 raises minimumAppBuild anyway, so no app calling the connector's endpoints uses the instance after it. An instance is to have one way to an assistant, its operator's (ADR 0021, proposed). The app: the field at the foot of Home, and the button in the Library, on a studyset and while studying, search the web alone, through the engine chosen in Settings or Android's own search, with the parts of a card the person picks; a studyset's name and description are never offered. Gone: the assistants a question opened in (AiAssistant, AssistantHandOff), the switch between them and the web, Settings' choice of assistant, Connect Claude with its route, cubit, repository and gateway, and their texts. A settings file from before keeps the old keys until the next change of a setting writes it anew; nothing reads them. The server: lib/src/assistant/ (MCP, its OAuth, the five tools, connect codes and tokens), AssistantEndpoint, the daily forgetting of the connector's secrets (its scheduled call cancelled at start), its lines in the deletion hook, instance.info().assistantConnectorUrl (nullable, so an older app reads it as none), and Caddy's forwarding of /mcp, /oauth/* and /.well-known/oauth-*. The migration drops the connector's five tables, children first: DROP TABLE "assistant_token" CASCADE; -- tokens of a connection DROP TABLE "assistant_grant" CASCADE; -- OAuth codes of a connection DROP TABLE "assistant_connection" CASCADE; -- a person's connected client DROP TABLE "assistant_connect_code" CASCADE; DROP TABLE "assistant_client" CASCADE; -- OAuth clients registered No other table references them, so CASCADE drops only their own foreign keys. For the minute between the maintenance run and the switch, the old server answers the connector with errors and fails to delete an account (its hook still deletes from these tables); ADR 0022 says so. BREAKING CHANGE: a connected Claude gets no answer from the deploy on, and the app no longer opens a question in an AI assistant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
82 lines
5 KiB
Markdown
82 lines
5 KiB
Markdown
# ADR 0006: Claude reads studysets through a connector on the instance
|
||
|
||
- **Status:** accepted; superseded on 23 Oct 2026 by ADR 0022, which took the connector
|
||
out (v0.19.0, build 22).
|
||
- **Date:** 2026-10-10
|
||
|
||
## Context
|
||
|
||
People want to ask an assistant about what they learn: explain a card, quiz them on a
|
||
studyset, say what to repeat. The assistant should be able to read their material,
|
||
reading only for now, writing perhaps later. The first one is Claude; others, such as
|
||
ChatGPT, should be possible.
|
||
|
||
Most people who use Claude pay for a plan (Free, Pro, Max), not for the API. A plan
|
||
can't drive a chat inside another app: Anthropic allows no third party to offer
|
||
sign-in with Claude or to use a plan's limits ("Unless previously approved, Anthropic
|
||
does not allow third party developers to offer claude.ai login or rate limits for their
|
||
products", Agent SDK documentation, enforced since spring 2026). An in-app chat would
|
||
need everyone to buy API access and paste a key.
|
||
|
||
Claude's apps, on every plan and on phones too, can add a **custom connector**: a remote
|
||
MCP server that Claude calls from Anthropic's cloud, signed in with OAuth. The
|
||
conversation then happens in Claude, and Claude reads through the connector's tools
|
||
whatever it needs. ChatGPT and other clients speak MCP as well.
|
||
|
||
## Decision
|
||
|
||
1. **The instance is the connector.** It already holds every person's content in
|
||
plaintext (ADR 0002) and is reachable from the internet, which a phone isn't. Its web
|
||
server serves the MCP endpoint (`/mcp`, Streamable HTTP, answering with JSON, no
|
||
sessions) and the OAuth it needs. Caddy forwards those paths to it; the API server is
|
||
untouched.
|
||
2. **Read-only tools**, the person's own content only, whoever's token it is:
|
||
studysets, their cards and notes, a search across cards, and progress. Each tool is
|
||
marked read-only. Writing comes later, as tools of its own behind a scope of its
|
||
own.
|
||
3. **Connecting by code.** Anonymous accounts can't sign in on a web page, so the
|
||
authorization page asks for a code the app shows (Settings › Assistant): eight
|
||
characters, valid for ten minutes, used once, kept only as a hash. The page names
|
||
where the person returns to (the client's redirect host), so a look-alike client
|
||
can't pass as Claude. The code is the consent; a wrong one is refused without
|
||
saying more.
|
||
4. **OAuth as MCP specifies (2026-07-28), the parts Claude uses:** protected resource
|
||
metadata (RFC 9728), authorization server metadata (RFC 8414), dynamic client
|
||
registration (RFC 7591; a public client, or one with a secret in the form or as
|
||
Basic), authorization code with PKCE (S256 only), the `resource` parameter bound
|
||
into every token (RFC 8707), `iss` in the response (RFC 9207), refresh tokens that
|
||
rotate. Redirect URIs are https, or http on the device itself, and compared
|
||
exactly; a request naming an unknown client or URI gets an error page, never a
|
||
redirect. Client ID metadata documents are not supported yet: fetching a client's
|
||
URL from the server needs care against requests into the instance's own network,
|
||
and clients fall back to registration.
|
||
5. **Tokens are kept as hashes** (SHA-256 of 256 random bits): an access token lives
|
||
an hour, a refresh token 30 days from its last use and only once. A connection is
|
||
the person's grant to one client, one per client, renewed by connecting again;
|
||
the app lists them and disconnects one, which ends its tokens at once. Deleting
|
||
the account deletes them; a daily job deletes what expired and clients nobody
|
||
connected within a week.
|
||
6. **The pages and the endpoint:** the authorization page loads nothing from
|
||
elsewhere, runs no script, and may not be framed (`frame-ancestors 'none'`); it
|
||
speaks English or German as the browser asks. The MCP endpoint takes POST only,
|
||
refuses pages from plain-http origins (DNS rebinding), and every request needs a
|
||
token; the tools are built per request for the token's person.
|
||
7. **In the app, an opt-in hand-off.** A device setting, off unless turned on, shows a
|
||
field on Home and a button in the Library, on a studyset and in a study session.
|
||
Each opens `claude.ai/new?q=…` with the question and what it is about written in
|
||
(the studyset's name, a card's text), for the person to read and send in Claude.
|
||
Nothing is sent from the app; what the conversation contains is Claude's, under
|
||
Anthropic's terms.
|
||
|
||
## Consequences
|
||
|
||
- People use the plan they have; Halkyon Learn pays for nothing and holds no API key.
|
||
- The answer appears in Claude, not inside Halkyon Learn. An in-app chat for people with
|
||
an API key can follow behind the same opt-in.
|
||
- The instance gains an OAuth authorization server, small but security-relevant: codes
|
||
and tokens hashed, redirect URIs matched exactly, PKCE required, the pages not
|
||
framable, every path tested.
|
||
- What Claude reads leaves the instance for Anthropic, at the person's request; the
|
||
privacy policy says so.
|
||
- Another instance offers the same once it runs this version; the app shows the
|
||
connector's address from what the instance says about itself.
|