ADR 0044's step 6b (D48) in the app: where an Editor has Add material, a Viewer of a shared studyset has Suggest material, offering a flashcard or a note. The composer and the note editor open suggesting from the start, saying that what is suggested joins the studyset once its owner or an editor takes it in; Send suggestion asks why, and Cancel or sending closes it. Nothing is written into the studyset: the fields go into the suggestion alone, which goes into the Viewer's own space as the changes they suggest do. Whats-New: In a studyset shared with you as a viewer, Suggest material proposes a new flashcard or note to its owner and editors. Whats-New-De: In einem Lernset, das du nur ansehen darfst, schlägst du über „Material vorschlagen“ neue Karteikarten und Notizen vor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
401 lines
29 KiB
Markdown
401 lines
29 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; D29 to D32 the agent's
|
|
recommendations, which they chose.
|
|
Steps 1 to 5 and step 6's first two parts 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.
|
|
|
|
- **D29.** **Share opens one sheet** (decided 11 Nov 2026, the agent's recommendation):
|
|
People first (the members with their roles, *Invite by @handle*, or *Share with
|
|
people…* for a studyset not shared yet), then the community part as before (privacy,
|
|
publishing), as Vaia's Share does.
|
|
- **D30.** **A Viewer's studyset screen is read-only in step 4** (decided 11 Nov 2026,
|
|
the agent's recommendation): it says *You can view and study this* and hides adding,
|
|
editing, moving and deleting; a write that slips through elsewhere, which the engine
|
|
refuses, says so rather than failing silently. Each editor is checked one by one
|
|
later.
|
|
- **D31.** **A shared studyset is marked with people** (decided 11 Nov 2026, the agent's
|
|
recommendation): Lucide's *users* in place of the lock, as it is always sealed (D21),
|
|
labelled *Shared · private*.
|
|
- **D32.** **Safety codes are compared from a member's row** (decided 11 Nov 2026, the
|
|
agent's recommendation): their screen shows the code in twelve groups of five and
|
|
*Mark as compared*; a member whose keys changed is flagged on their row, and their
|
|
new keys are trusted on that screen, after comparing. Members are matched to their
|
|
keys by handle, as the member list names nobody by account ID.
|
|
- **D33.** **A writer's device rolls the key at its next upkeep** (decided 11 Nov 2026,
|
|
the agent's recommendation, reading D22): the instance marks a space whose member
|
|
went (removed, left or deleted; an invitation taken back never had a key), and the
|
|
Owner's or an Editor's device that next keeps its keys up after a sync makes the new
|
|
key and seals it to the members. Two devices rolling at once make two keys, both
|
|
sealed to everyone, the later one taken: harmless.
|
|
- **D34.** **A new key sealed clears the mark** (decided 11 Nov 2026, the agent's
|
|
recommendation): the first seal bringing a key ID the space never had, in the same
|
|
transaction. Members the rolling device missed (keys changed, not trusted anew) get
|
|
it later as newcomers do; waiting for every member would roll again and again.
|
|
- **D35.** **A Viewer opens cards, notes and steps read-only** (decided 11 Nov 2026, the
|
|
agent's recommendation, reading D30): the same screens as an Editor's, their content
|
|
shown but not editable, without Save, saying the studyset is theirs to view and
|
|
study; the lists' menus offer them nothing that writes.
|
|
- **D36.** **Burying and reporting a card are hidden in shared studysets for now**
|
|
(decided 11 Nov 2026, the agent's recommendation): both are written on the card's
|
|
row, so one member's would be everyone's, and a Viewer's would be refused. They return
|
|
once they are each person's own rows by the card's ID, as reviews are (D26), a change
|
|
of the synced tables shipped with a release.
|
|
- **D37.** **An email invitation admits only who proves the address** (decided 11 Nov
|
|
2026, the agent's recommendation, reading D4): accepting its link asks for a code mailed
|
|
to the invited address (halkyon_base's email codes), so a forwarded or leaked mail
|
|
admits nobody else; it lapses after 14 days, and someone without an account signs up
|
|
first.
|
|
- **D38.** **The instance keeps an invited address as a keyed hash alone** (decided 11
|
|
Nov 2026, the agent's recommendation): the mail goes out as the invitation is made,
|
|
then only HMAC-SHA256 of the address in lower case stays (ADR 0010's pepper), to check
|
|
the proof against. Members see "invited by email", never the address; resending means
|
|
typing it again.
|
|
- **D39.** **A link opens a page on the instance, then the app** (decided 11 Nov 2026,
|
|
the agent's recommendation): an email invitation's link and a space's join link (D5)
|
|
are pages (`/join/…`) saying what they are and to open Halkyon Learn, with a button
|
|
opening the app where it can (Android's app links) and the code to paste in the
|
|
Library's *Join with a link* everywhere else.
|
|
- **D40.** **What is shared with someone shows in their Inbox and on its card**
|
|
(decided 11 Nov 2026, the agent's recommendation): Profile's Inbox lists "@alice shared
|
|
Japanese with you" with Open and Leave, counted in its badge until seen, and the
|
|
studyset's library card says New until it is opened.
|
|
|
|
- **D41.** **The agent shares Learn's code rather than repeating it** (decided 11 Nov 2026,
|
|
the agent's recommendation): what reads and seals Learn's content (its models, the
|
|
payload codecs, the envelope, halkyon_app's `SharedKeys` and `KeyPins`) moves into
|
|
pure-Dart packages the app and the agent both use, and the agent syncs its spaces
|
|
through the engine's client (pure Dart) as an app does. Over an endpoint handing the
|
|
agent a space's rows with a codec of its own, which would keep the privacy rules in
|
|
two places, and over readable studysets alone, which would leave D6's keys for later.
|
|
- **D42.** **Step 6 is built in three parts** (decided 11 Nov 2026, the agent's
|
|
recommendation): agent accounts and their tokens in the module, made and revoked in
|
|
the Profile (D6, D12); suggestions within a shared space in Learn (D10); then the
|
|
agent itself, its CLI and MCP server (D7, D41). Each is usable and tested alone.
|
|
- **D43.** **An agent's token is exchanged for a session** (decided 11 Nov 2026, the
|
|
agent's recommendation): sent once to the module's sign-in for agents, it gets an
|
|
ordinary server-side session, so every endpoint, scope and revocation holds as for
|
|
people; the instance keeps the token's hash alone and stamps its last use at each
|
|
sign-in (D12), and revoking it ends the agent's sessions. Over the token on every
|
|
call, a second way of authenticating beside Serverpod's sessions.
|
|
- **D44.** **Whoever may not edit suggests** (decided 11 Nov 2026, the agent's
|
|
recommendation): an agent, and a person who is a Viewer, suggest changes within a
|
|
shared studyset, which an Editor or its Owner accepts or declines (D10 widened).
|
|
- **D45.** **Agent accounts as the register keeps them** (decided 11 Nov 2026, the
|
|
agent's recommendations): an agent is an account of the instance's register
|
|
(`agent_account`: its owner, its name), up to 5 a person, made from the Profile by an
|
|
identified person who is no agent, where the operator didn't switch agents off
|
|
(`AGENTS=off`, on otherwise, said on the privacy page). Its token (`agent_token`, one
|
|
an agent, 32 random bytes kept as their SHA-256 alone, shown once) signs it in for a
|
|
day at most; the agent signs in at each start. Its sessions carry `identified` and
|
|
`agent`, and an agent makes no agents, creates or owns no shared space, is invited and
|
|
raised as a Viewer alone, uses none of the operator's assistants, operates nothing
|
|
and signs in no other way. It sets its public keys itself, once. Deleting the owner
|
|
deletes their agents first, as accounts are deleted; its public profile says whose
|
|
agent it is.
|
|
- **D46.** **A suggestion in a shared space is kept as Explore's are** (decided 11 Nov
|
|
2026, the agent's recommendation): in its writer's own synced space, so it is written
|
|
offline, sealed with the shared studyset's key where that is private; the instance
|
|
gathers each space's for its members, checking membership and role as it reads.
|
|
`feedback_thread` takes it as kinds of its own (`space-suggestion`, `space-report`),
|
|
its `publicationId` holding the space's id, so no synced table changes and older apps,
|
|
skipping kinds they don't know, keep syncing. Editors' and the Owner's decisions are
|
|
written into the shared space itself, where the engine already allows them alone.
|
|
Over rows of the shared space written by Viewers, which would need the engine to let
|
|
a read-only member write some tables.
|
|
- **D47.** **Every member sees them** (decided 11 Nov 2026, the agent's recommendation):
|
|
a suggestion or report in a shared space, and its replies, show to everyone in it,
|
|
Viewers too, as Explore's show to everyone learning a studyset.
|
|
- **D48.** **Changes and new items** (decided 11 Nov 2026, the agent's
|
|
recommendation): a Viewer or an agent suggests a changed card, note or step, as on
|
|
Explore, and new cards and notes for the studyset or one of its topics; deleting is
|
|
asked for with a report. A suggestion may name the files the studyset holds; new
|
|
files it would bring come later.
|
|
- **D49.** **What the agent needs moves, the rest later** (decided 11 Nov 2026, the
|
|
agent's recommendation): `packages/halkyon_content` (pure Dart) takes halkyon_app's
|
|
envelope (`PayloadEnvelope`, `ContentCipher`) and the shared keys (`SharedKeys`,
|
|
`KeyPins`), which halkyon_app re-exports; `apps/learn/halkyon_learn_core` (pure Dart)
|
|
takes Learn's privacy, sync, content and the models, codecs, gateways and
|
|
repositories of studysets, cards, notes, tags and feedback, which the app imports
|
|
from there. Paths, PDFs and files follow when the agent needs them. Over moving
|
|
every Flutter-free data layer at once (a larger move than the agent asks for), and
|
|
over one package for both (Todo and Train would depend on Learn's).
|
|
**Amended 11 Nov 2026, in building it**: what Learn's data layer reaches went too, all
|
|
pure Dart already and re-exported where it was: rich text (`RichContent`, from
|
|
halkyon_editor into halkyon_content's `rich_text.dart`), people's files on the device
|
|
and the module's store (`FileCache`, `FileServerGateway`, from halkyon_app),
|
|
`DelayedDeletions` into halkyon_core, and of Learn's what feedback and privacy take:
|
|
the files' repository, learning paths' models and repository, citations, study
|
|
options. The synced gateways take halkyon_core's `SyncRequests` in place of
|
|
halkyon_app's `SyncService`, which implements it; a subject's icon, colour and name
|
|
stay in the app (`SubjectLook`), its ID and group in the core.
|
|
- **D50.** **The agent keeps its keys in files only its user reads** (decided 11 Nov
|
|
2026, the agent's recommendation): `$XDG_CONFIG_HOME/halkyon-agent/<instance>/`,
|
|
mode 0700, its files 0600: the token, the key pair, the pins and the local sync
|
|
database. The agent runs as a user of its own (D7), whose boundary protects them,
|
|
and starts unattended; `halkyon-agent sign-in` takes the token once from stdin.
|
|
Over a passphrase sealing the key (which helps only where it is kept apart from the
|
|
files) and over a system keyring (none unlocked for a headless user).
|
|
- **D51.** **Claude Code reaches it as a stdio MCP server on `dart_mcp`** (decided 11
|
|
Nov 2026, the agent's recommendation): one executable, `halkyon-agent sign-in |
|
|
sync | mcp`; its tools list the shared studysets, read a studyset's cards and notes,
|
|
search cards, suggest a change or a new card or note, report something to delete,
|
|
list a studyset's suggestions and reply, syncing before reading and after writing.
|
|
Over JSON-RPC by hand (MCP's revisions to keep up with) and over a CLI alone.
|
|
- **D52.** **Built on the laptop, copied by hand** (decided 11 Nov 2026, the agent's
|
|
recommendation): `dart build cli` bundles libsodium and SQLite as native assets,
|
|
and a script beside the agent copies the bundle to its user on the host, which has
|
|
no Dart SDK. The release tool takes it on once it has settled. Over a release of its
|
|
own now, and over a container (D7 runs it without Docker).
|
|
|
|
## 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.
|
|
Built 11 Nov 2026 (D27 to D34): the copy into a space of its own, the Share sheet
|
|
with its members, roles, leaving and safety codes, a Viewer's screens read-only,
|
|
the key rolled once a member goes, and a Viewer's material opened read-only (D35),
|
|
burying and reporting hidden in shared studysets (D36).
|
|
5. **Invitations by email and by link** (D4, D5), the Inbox.
|
|
The instance's half built 11 Nov 2026 (D37 to D40): `sharedSpace.inviteByEmail`
|
|
mails through the register (`send-space-invitation`), naming who invites by their
|
|
handle (refused without one) and nothing of what is shared, and keeps
|
|
`shared_space_email_invitation` (the address's keyed hash, the SHA-256 of the
|
|
link's token, the role); `acceptEmailInvitation` takes a code asked for
|
|
`EmailCodePurpose.joinSpace`, proven through the register (`prove-address-code`),
|
|
and refuses one proving another address (`otherAddress`); inviting the address
|
|
again replaces the invitation, and the hourly pruning forgets those lapsed. A
|
|
space's link (`setJoinLink`, the Owner's; `joinLink`, whoever may invite;
|
|
`joinByLink`, as a Viewer) keeps its token as it is in `shared_space.joinToken`,
|
|
unlike a mailed one, so that it can be copied again: the instance decides who is
|
|
a member anyway, and switching the link off and on makes a new token, ending the
|
|
old one. `preview` and the page at `/join/<token>` (`addJoinPageRoute`, English and
|
|
German, `noindex`, `no-store`) say who invites and as whom, alike 404 for a token
|
|
that never was, lapsed or was switched off. A membership someone else made is new
|
|
to its member (`shared_space_member.isNew`, `addedBy`; `mine` names who shared it,
|
|
`markSeen` clears it); joining by a link or a mailed invitation is not.
|
|
The apps' half built 11 Nov 2026: halkyon_app's `SharedSpaceGateway` asks all of it
|
|
(`joinTokenOf` reads a link or its token pasted), its fake keeping the mails and
|
|
codes it would send; in Learn the Owner's Share sheet invites by handle or by
|
|
email and switches the link, copying it; the Library's menu joins with a link
|
|
pasted, and a link opened on Android (`/join/<token>`, verified with the
|
|
instance's assetlinks) opens the same on a screen of its own, asking for the
|
|
address and its code where the invitation was mailed; what is new shows in the
|
|
Inbox ("@alice shared Japanese with you", Open or Leave), on the Profile tab's
|
|
badge and as New on the studyset's card, until the studyset is opened.
|
|
6. **Agents** (D6, D7, D10, D12), in three parts (D42).
|
|
a. **Agent accounts** (D43, D45), built 11 Nov 2026: the module's `agent_account`
|
|
and `agent_token`, kept by the register and asked by its members;
|
|
`agents.create`, `mine`, `newToken`, `revoke`, `delete` and `setKeys` for the
|
|
owner, `agentSignIn.signIn` exchanging the token for a day's session with the
|
|
scopes `identified` and `agent`, at most 60 sign-ins an hour a network; a token
|
|
unused for 90 days lapses in each service's pruning. The refusals of D45 have
|
|
their denial tests (an agent making agents, creating or owning a space, invited
|
|
or raised above Viewer, handed a space on, named operator, granted assistants).
|
|
`instance.basics().agents`, the privacy page's sentence, `Profile.agentOf`. In
|
|
the apps, halkyon_app's `AgentsSection` in every Profile (made, token shown
|
|
once to copy, new token, taken back, deleted) and members marked as agents
|
|
(`SpaceMember.agent`; Learn's Share sheet: a bot, "Agent", no role above
|
|
Viewer offered, none to hand the studyset on to).
|
|
b. **Suggestions within a shared space** (D10, D44, D46 to D48), built 11 Nov
|
|
2026: Learn's `feedback.threadsInSpace` gathers a space's threads
|
|
(`space-suggestion`, `space-report`) for its members alone, its writers
|
|
and repliers members now, its decisions read from the space's own rows;
|
|
the inbox adds those the caller decides on or wrote; a sealed payload
|
|
passes in base64 with its key and nonce (`FeedbackView.space`, `keyId`,
|
|
`nonce`). The module tells a host a person's spaces and each space's
|
|
members (`sharedSpacesOf`, `membersOfSpaces`). In the app,
|
|
`FeedbackRepository` suggests changed cards, notes and steps and new
|
|
cards and notes, reports, replies (sealed for their thread) and decides
|
|
in the space (`SpaceRouter.placeRow`), and takes a suggestion in only
|
|
where it names the studyset's own files and brings none; a Viewer's
|
|
card and note offer Suggest a change, and a shared studyset's menu its
|
|
Suggestions, where its Owner and Editors accept or decline. New cards
|
|
and notes are suggested through the repository (the agent, step 6c),
|
|
and in the app from a Viewer's Suggest material, in place of Add
|
|
material: the composer or note editor opens suggesting from the
|
|
start, Cancel or Send closing it, nothing added.
|
|
c. The agent itself, its CLI and MCP server, on a pure-Dart core (D7, D41, D49 to
|
|
D52).
|
|
|
|
## 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.
|