ADR 0044, D27 and D28. - D27: sharing a studyset with a publication anywhere in its tree is refused for now. Publishing reads a release from the person's readable rows on the instance, and a shared studyset is sealed in another space, so it could take no release; a later step has the device hand over a release's content and re-points the publication. - D28 (the agent's, to be confirmed): the copy is the archive's export and import, so progress, tags, stars and the archive follow, each item keeping its link to Explore and each card its origin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
194 lines
13 KiB
Markdown
194 lines
13 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, and D15 to D26;
|
|
D27 their choice; D28 the agent's, to be confirmed.
|
|
Steps 1 to 3 built; the rest 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.
|
|
- **D20.** **A row in a shared space is trusted when any member this device pinned
|
|
signed it** (decided 11 Nov 2026, the agent's recommendation): current or former,
|
|
never by the author column a row names, which the instance could alter; a few
|
|
signature checks a row, for a group of a few people.
|
|
- **D21.** **A shared space is always sealed** with its key (decided 11 Nov 2026, the
|
|
agent's recommendation): sharing needs keys, so someone without a password sets one
|
|
first, and what goes public goes through *Share with the community*. ADR 0019's
|
|
readable records stay out of shared spaces.
|
|
- **D22.** **Rolling the key seals new writes alone** (decided 11 Nov 2026, the agent's
|
|
recommendation, reading D3): new and changed rows take the new key, earlier ones stay
|
|
under the key they had, which the members keep, since whoever went had read them
|
|
already. The instance marks a space whose member went, and the next writer's device
|
|
makes the new key and seals it to the members.
|
|
- **D23.** **A shared studyset is at the top level for every member** (decided 11 Nov
|
|
2026, the agent's recommendation): sharing takes it with its subtree out of its
|
|
parent, marked as shared; placing it in one's own tree may come later. Invited by
|
|
@handle, it appears in the invitee's list, which they may leave; the Inbox is step 5.
|
|
|
|
- **D24.** **Writes reach a shared space through the engine** (decided 11 Nov 2026, the
|
|
agent's recommendation): found 11 Nov 2026, every gateway writes in the personal space
|
|
and an update of a shared row finds none. Our sync fork updates and deletes a row in
|
|
its own space, checked against the person's role there, and asks the host which space
|
|
a new row goes in, which Learn answers in one place (a card by its studyset, a
|
|
revision by its card), so no gateway changes and none is missed; wrapping each write
|
|
in `transactionForUser` was the other way.
|
|
- **D25.** **Tags stay each member's own** (decided 11 Nov 2026, the agent's
|
|
recommendation): in their personal space under their personal key, as reviews (D2).
|
|
|
|
- **D26.** **Personal rows name shared content by a plain ID** (decided 11 Nov 2026, the
|
|
agent's recommendation): found 11 Nov 2026 by experiment, the engine blanks a
|
|
reference from one space to another (its rule, *foreign keys stay within a space*),
|
|
and reviews, tags (cards', notes', paths'), PDF readings and path answers are
|
|
personal (D2, D25) but name cards, notes, paths, steps and PDFs. Those six tables
|
|
keep their columns without the database relation; Learn deletes what names an item
|
|
as it deletes the item, and upkeep prunes rows naming one gone. Putting them in the
|
|
shared space would show members each other's reviews; teaching the engine to link
|
|
across spaces goes against its design.
|
|
|
|
- **D27.** **A published studyset isn't shared, for now** (decided 11 Nov 2026, the
|
|
developer's choice of the agent's corrected recommendation): publishing reads a
|
|
release from the person's readable rows on the instance (`readReleaseFiles`,
|
|
refusing a private studyset), and a shared studyset is sealed (D21) in another
|
|
space, so it could take no release. Sharing a studyset with a publication anywhere in
|
|
its tree, withdrawn or not, is refused, saying why, until publishing takes a
|
|
release's content from the device, a step of its own that re-points the publication
|
|
to the shared studyset then.
|
|
- **D28.** **The copy is the archive's export and import** (11 Nov 2026, the agent's
|
|
choice, for the developer to confirm): the tree written out with its progress, as a
|
|
backup is, and read back under new IDs into the space through the person's
|
|
repositories, so reviews, answers, readings, tags by name, stars and the archive
|
|
follow as an import's do and no rewrite of its own is needed; unlike a backup, it
|
|
keeps each item's link to Explore (`published`) and each card's origin, as the
|
|
studyset is the same one moved. The old tree is deleted for good once the copy is
|
|
whole; a copy failing or aborted deletes what it wrote and the space it made.
|
|
|
|
## 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, D20 to D26): 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.
|