81 lines
4.9 KiB
Markdown
81 lines
4.9 KiB
Markdown
|
|
# ADR 0006: Claude reads studysets through a connector on the instance
|
|||
|
|
|
|||
|
|
- **Status:** accepted
|
|||
|
|
- **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.
|