Step 3 of ADR 0044 (D3, D18, D19). A shared space's data keys are sealed on a device to each member's public keys; the instance keeps what devices sealed and never opens it. - halkyon_keys: the safety code of two accounts' public keys (`safetyCode`), 60 digits, the same on both sides. - halkyon_base: the register hands out accounts' public keys (`public-keys-of`, local and remote); the table `shared_space_key` and the endpoint `sharedSpaceKeys` (memberKeys, keysNeeded, seal, mine). Only the Owner and Editors seal, the first key sealed to a member stays, and a member's keys go with their membership. Every host's migration creates the table. - halkyon_app: `ContentCipher` seals a key to another account and opens one sealed by another; `SharedKeys` shares a space's key with its members, seals keys to newcomers and opens the person's own, holding each member to the public keys `KeyPins` pinned on first sight (`key_pins.json` in the person's folder), a change refused until trusted anew. Learn takes them into its `Privacy` in step 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
139 lines
9.3 KiB
Markdown
139 lines
9.3 KiB
Markdown
# ADR 0044: Shared spaces and collaboration, people and agents alike
|
|
|
|
- **Status:** decided 10 Nov 2026 by the developer: D8 is their own request, D9 and D10
|
|
their choices over the agent's recommendations (all ticked at first; agents editing
|
|
directly), D1 to D7 the agent's recommendations, which they chose; D11 to D13 the
|
|
agent's recommendations, which they chose on 11 Nov 2026, D14 too. Not built yet;
|
|
the steps below.
|
|
- **Builds on:** ADR 0005 (publishing, reports and suggestions), ADR 0014 (the module),
|
|
ADR 0016 (one account across an instance), ADR 0017 (key pairs), ADR 0019 (private
|
|
studysets), ADR 0021 (operators and assistants).
|
|
|
|
## Context
|
|
|
|
A studyset belongs to one person: their rows are in their personal space, a private
|
|
one sealed with a data key that only their own devices unwrap. The developer asked (10
|
|
Nov 2026) for Vaia's sharing (*Manage collaborators*: invite by email, an invite link,
|
|
the members and their roles; *Collaboration settings*: joining by link, collaborators
|
|
inviting others; *Share with the community*: choosing what material goes public) and
|
|
for collaboration with everyone, people and agents alike: *invite @Paul or @Claude to a
|
|
studyset*, Claude Code on halkyon.li's host working as an agent account, as Copilot or
|
|
Claude work on a GitHub repository.
|
|
|
|
The sync engine already has shared spaces (`OfflineSyncSpaces`: create, grant, revoke,
|
|
members), with two roles, `readOnly` and `readWrite`, which limit writes alone: a
|
|
member reads everything in the space.
|
|
|
|
## Decisions
|
|
|
|
- **D1.** **Shared spaces live in halkyon_base,** for every app: members, roles,
|
|
invitations, links, settings and the wrapped keys. Learn's studysets use them first,
|
|
Todo's projects later on the same tables.
|
|
- **D2.** **Roles: Owner, Editor, Viewer.** The Owner can do everything, delete the
|
|
studyset and hand it to another member; an Editor changes content; a Viewer reads and
|
|
studies, with their own reviews in their own space. Editor and Owner are the engine's
|
|
`readWrite`, a Viewer its `readOnly`; who is Owner is the module's own table, checked
|
|
by its endpoints.
|
|
- **D3.** **A private studyset's key is wrapped to each member's public key** (ADR
|
|
0017's key pairs) by a member's device that holds it, at its next sync after someone
|
|
joined. The instance never sees the key; reading waits for that sync. A member
|
|
removed gets no new content: the studyset rolls to a new key, as ADR 0019's rewrites.
|
|
- **D4.** **Invites by @handle or by email.** An @handle adds a person on the instance at
|
|
once (they see it in their Inbox and may leave). An email address gets a mail with a
|
|
link to join, which works after signing up; the address is never shown to the other
|
|
members.
|
|
- **D5.** **Joining by link is off at first;** someone joining by it is a Viewer, whom
|
|
the Owner can raise. *Collaborators may invite others* is off at first too.
|
|
- **D6.** **Agents are accounts of their own,** marked as agents, each owned by a person
|
|
who made it from their Profile; listed in the owner's Profile, deleted with them. An
|
|
agent has its own key pair, kept where it runs, signs in with a token the owner made
|
|
(never a password), and is invited like a person: *@claude*.
|
|
- **D7.** **An agent reaches the instance through a small CLI or MCP server** run beside
|
|
it (Claude Code on halkyon.li's host runs as its own user, without Docker): it signs
|
|
in with its token, syncs the spaces it is a member of, opens the sealed rows with its
|
|
own keys, and writes through the same endpoints an app uses.
|
|
- **D8.** **Share with the community** (publishing to Explore, ADR 0005) gets a step
|
|
confirming the material: every PDF, other file and note listed by kind.
|
|
- **D9.** That step's choice: **all unticked at first**, with per kind *tick all*, and
|
|
on any item *everything but this* and *nothing but this*. Cards and the learning path
|
|
always go.
|
|
- **D10.** **An agent suggests; a person accepts.** An agent is a Viewer in the engine's
|
|
sense and writes nothing into the studyset; its changes go as suggestions, which a
|
|
member with Editor's rights accepts or declines, each marked as by @claude. Learn's
|
|
reports and suggestions (ADR 0005, stage 4: `feedback_thread`, the author's
|
|
decisions, accepted suggestions applied by their item) are the start: they become
|
|
suggestions within a shared space too, not only on published releases.
|
|
|
|
- **D11.** **A shared studyset passes on.** Leaving, the Owner picks the next Owner
|
|
first. Deleting their account, ownership passes by itself to the Editor who joined
|
|
first, else to the Viewer who joined first; only with no other member does the
|
|
studyset go with them.
|
|
- **D12.** **An agent's token lapses after 90 days unused;** while used it has no fixed
|
|
end, the owner revokes it any time, and their Profile shows when it was last used.
|
|
- **D13.** **An invitation by @handle waits** for someone who has an account on the
|
|
instance's register but has never used the app: they find it in their Inbox the first
|
|
time they sign in to it, and the inviter sees them as invited until then.
|
|
|
|
- **D14.** **Sharing copies the studyset under new IDs** into the shared space, rewrites
|
|
what names its rows (reviews, tags, the learning path, files) and deletes the old ones,
|
|
as ADR 0019's rewrites do; the fork stays as it is. Found 11 Nov 2026: the engine's
|
|
delete is soft (`insteadOfDelete`: the domain row stays, hidden by a tombstone in its
|
|
space's `crdt_data_rows`), and a synced table's primary key is the row's ID alone, so
|
|
the same ID can't be inserted into another space; teaching the engine to move a row
|
|
was the other way, set aside as a deeper change to its merge, kept on every rebase.
|
|
|
|
- **D15.** **The module reaches the sync engine through its host** (decided 11 Nov 2026,
|
|
the agent's recommendation): each host hands halkyon_base a `SpaceGrants` (create a
|
|
space, grant, revoke, delete), made with `OfflineSyncSpaces`, as it hands a
|
|
`FileStore` (ADR 0043), so the module stays free of the engine and its tests run on a
|
|
stand-in.
|
|
- **D16.** **A shared space's tables are its service's own** (decided 11 Nov 2026, the
|
|
agent's recommendation): a Learn studyset's space and members are in Learn's
|
|
database, beside the engine's space they mirror, a Todo project's in Todo's; members
|
|
are the register's account IDs, their handles asked of the register as now.
|
|
- **D17.** **Never used the app means no account on its service yet** (decided 11 Nov
|
|
2026, the agent's recommendation, reading D13): someone without an `AuthUser` row on
|
|
the service gets a waiting invitation, made a membership the first time they sign in
|
|
there; on the register, where everyone has one, the invited are members at once.
|
|
- **D18.** **A member's public keys are pinned, a change refused, a safety code
|
|
compared** (decided 11 Nov 2026, the agent's recommendation): only the instance hands
|
|
a device a member's keys, and one that swapped in its own would read every studyset
|
|
shared with it. So each device keeps the keys it first saw for each person (as Signal
|
|
and SSH do), seals nothing to keys that changed and says so, and shows a safety code
|
|
made of both people's keys, which two members compare in person or by call; a swap
|
|
after the first sight is caught, one before it by comparing.
|
|
- **D19.** **Step 3 builds the module's wrapped keys and halkyon_app's device side**
|
|
(decided 11 Nov 2026, the agent's recommendation), each tested; Learn takes them into
|
|
its `Privacy` with the copy in step 4.
|
|
|
|
## Steps
|
|
|
|
1. **Share with the community's confirmation** (D8, D9), needing nothing else here.
|
|
Built 11 Nov 2026.
|
|
2. **Shared spaces in halkyon_base** (D1, D2, D11, D13): the module's tables (a shared
|
|
space, its members and roles, its Owner), its endpoints with their denial tests,
|
|
the engine's grants kept in step, the hand-on as an account is deleted. Built 11 Nov
|
|
2026: the module's `sharedSpace` endpoint, Learn's `SyncSpaceGrants`; Todo hands over
|
|
no engine yet, so its server refuses (`unavailable`) until its projects are shared.
|
|
3. **Keys** (D3, D18, D19): a private studyset's key wrapped to each member's key pair.
|
|
Built 11 Nov 2026: the register hands out members' public keys (`public-keys-of`),
|
|
the module keeps the keys devices sealed (`shared_space_key`, its `sharedSpaceKeys`
|
|
endpoint, a Viewer sealing none, a member's keys going with their membership);
|
|
halkyon_app seals and opens them (`SharedKeys`), pinning each member's public keys
|
|
on first sight (`KeyPins`), with the safety code of halkyon_keys (`safetyCode`).
|
|
4. **Learn shares a studyset** (D14): the copy, *Manage collaborators* (invite by
|
|
@handle, the members, their roles, leaving), each member's own reviews.
|
|
5. **Invitations by email and by link** (D4, D5), the Inbox.
|
|
6. **Agents** (D6, D7, D10, D12).
|
|
|
|
## Open questions, before building
|
|
|
|
- **Moving rows between spaces.** Decided: D14.
|
|
- **Whose reviews.** A Viewer's or Editor's schedule for a shared card stays in their own
|
|
space, keyed by the card's ID; the card's own review rows today assume one learner.
|
|
- **Handing on and leaving.** Decided: D11. To build: the account's deletion hands the
|
|
shared spaces it owns on before it deletes the person's own.
|
|
- **Members across services.** Decided: D13. A member of a Learn studyset needs no Todo
|
|
account, and the reverse.
|
|
- **The agent's token:** made and revoked in the owner's Profile, scoped to the agent's
|
|
account, never shown again after making; its lapse decided: D12.
|