@dxos/halo 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/MIGRATION.md ADDED
@@ -0,0 +1,254 @@
1
+ # HALO → Keyhive Migration
2
+
3
+ Plan for rewriting HALO — replacing the protocol-buffer credential definitions and the hypercore-backed keyring/feed machinery — on top of [Keyhive](https://www.inkandswitch.com/keyhive/notebook/), the capability/encryption layer being developed as part of the Automerge ecosystem.
4
+
5
+ Status: draft. Backwards compatibility is explicitly out of scope at this stage.
6
+
7
+ See [API_AUDIT.md](./API_AUDIT.md) for the consumer-side audit (Composer/plugins) and the proposed Effect service surface that fronts this migration.
8
+
9
+ ## 1. Current HALO implementation
10
+
11
+ ### 1.1 Architecture
12
+
13
+ HALO is a credential system layered over append-only, signed hypercore feeds. Every identity owns a **HALO space** (a special space whose control feed carries the identity's credentials). Regular (ECHO) spaces carry their own credential stream in their **control feed**, separate from the **data feed** (database mutations).
14
+
15
+ | Package | Role |
16
+ | --------------------------------------------- | ------------------------------------------------------------------------------------------ |
17
+ | `@dxos/protocols` (`proto/dxos/halo/*.proto`) | Protobuf definitions: `Credential`, `Claim`, `Proof`, assertions, invitations, keys |
18
+ | `@dxos/credentials` | Credential factory/verifier, presentations, state machines, credential DAG |
19
+ | `@dxos/keyring` | ECDSA-P256 key storage (RAM/file or SQLite), signing; private keys never leave the keyring |
20
+ | `@dxos/client-services` | `IdentityManager`, invitation host/guest state machines, contacts service |
21
+ | `@dxos/echo-host` | Pipelines that feed credentials from feeds into the state machines |
22
+
23
+ ### 1.2 Key types
24
+
25
+ **Credential** (the universal envelope):
26
+
27
+ ```protobuf
28
+ message Credential {
29
+ PublicKey id; // SHA-256 of signed payload.
30
+ PublicKey issuer; // Space, identity, or device key.
31
+ Timestamp issuance_date;
32
+ Claim subject; // { id: PublicKey, assertion: Any }
33
+ Proof proof; // { type, signer, nonce, value, chain? }
34
+ repeated PublicKey parent_credential_ids; // DAG for concurrent updates.
35
+ }
36
+ ```
37
+
38
+ A `Proof.chain` carries a nested credential (`AuthorizedDevice`) proving a device may sign on behalf of an identity. `Presentation` bundles credentials + proofs (with nonces) for challenge/response.
39
+
40
+ **Assertions** (payload of `Claim.assertion`):
41
+
42
+ | Assertion | Purpose |
43
+ | ------------------------------------------------------- | --------------------------------------------------------------------------------- |
44
+ | `SpaceGenesis` | First message in a space's genesis feed; sets tags + membership policy |
45
+ | `SpaceMember` | Admit/remove identity; role: `OWNER`, `ADMIN`, `EDITOR`, `READER`, `REMOVED` |
46
+ | `MemberProfile` | Per-space member profile update |
47
+ | `AdmittedFeed` | Permit a feed to replicate in a space (`CONTROL` \| `DATA`) |
48
+ | `AuthorizedDevice` | Authorize device key to sign for identity (device chain) |
49
+ | `DeviceProfile` | Device metadata (`label`, `platform`, `os`, `type`: BROWSER/NATIVE/AGENT/MOBILE…) |
50
+ | `IdentityProfile` | Identity profile (`display_name`, `avatar_cid`, custom `data`) |
51
+ | `HaloSpace` | Links identity key → its HALO space key |
52
+ | `IdentityRecovery` | Recovery key (passkey/seedphrase) for identity |
53
+ | `DelegateSpaceInvitation` / `CancelDelegatedInvitation` | Offline-redeemable invitations |
54
+ | `Epoch` | Snapshot/GC markers |
55
+
56
+ **Keyring**: `KeyRecord { public_key, secret_key }`; keys typed IDENTITY / DEVICE / SPACE / FEED. ECDSA-P256 via SubtleCrypto; storage is file-per-key or SQLite.
57
+
58
+ **Contacts**: not stored — `ContactsService` derives `Contact { identityKey, profile, commonSpaces }` on the fly from space memberships.
59
+
60
+ ### 1.3 Key flows
61
+
62
+ **Identity creation** (`IdentityManager.createIdentity`):
63
+
64
+ 1. Generate identity, device, control-feed, data-feed, and HALO-space keys.
65
+ 2. Write to the HALO control feed: `SpaceGenesis`, `SpaceMember(identity, ADMIN)`, `AdmittedFeed(data)`, `AuthorizedDevice(identity → device)`, `DeviceProfile`, optional `IdentityProfile`.
66
+ 3. Identity is ready when the device chain (`AuthorizedDevice`) has been processed.
67
+
68
+ **Device join (HALO invitation)**: host and guest run an interactive state machine (INIT → CONNECTING → … → SUCCESS) over a swarm keyed by the invitation. Guest introduces itself, authenticates (shared secret or known-key challenge), sends its device + feed keys; host writes `AuthorizedDevice` + `AdmittedFeed` credentials and returns them with genesis hints.
69
+
70
+ **Space invitation**: same interactive machinery, but host writes `SpaceMember` (+ `AdmittedFeed`) for the guest identity. **Delegated** invitations write a `DelegateSpaceInvitation` credential replicated to all members, so any admin device — or, for `KNOWN_PUBLIC_KEY` auth, the guest itself — can complete admission offline; redemption is linked via `parent_credential_ids`.
71
+
72
+ **Access control / credential processing**: each space runs a `SpaceStateMachine` that verifies signatures (and delegation chains), then dispatches to `MemberStateMachine`, `FeedStateMachine`, `InvitationStateMachine`, plus identity-level `DeviceStateMachine` / `ProfileStateMachine`. Concurrent conflicting updates (e.g. two admins removing each other) are resolved via a **credential DAG** (`parent_credential_ids`): merge points wait for all paths, `tryPickWinningUpdate` breaks ties (OWNER wins), losing paths are replayed with overrides.
73
+
74
+ **Replication auth**: on connection, peers exchange signed auth messages; `TrustedKeySetAuthVerifier` checks the peer device is in the authorized set; feeds then replicate. Credentials gate replication (membership) but **content is not end-to-end encrypted** — possession of feeds implies readability.
75
+
76
+ ## 2. Keyhive
77
+
78
+ Keyhive is a local-first authorization and encryption layer from Ink & Switch (Rust core, WASM/TS bindings; pre-alpha). It provides group membership as a CRDT, continuous group key agreement (BeeKEM), and end-to-end encryption designed to compose with Automerge sync (Beelay → **Subduction**).
79
+
80
+ ### 2.1 Core concepts
81
+
82
+ - **Everything is a public key.** All principals — users, devices, groups, documents — are identified by Ed25519 keys. `Agent` = `Active` (local user) | `Individual` | `Group` | `Document`. A `Document` is a `Group` that also owns content; `Membered` = Group | Document.
83
+ - **Convergent capabilities**: a certificate-capability model compatible with CRDTs. Authority flows through signed **delegations** between keys; no consensus, no central authority, verification is offline.
84
+ - **`Access` levels** (ordered; each implies the previous): `Pull` (replicate ciphertext only — what sync servers get) < `Read` (decrypt) < `Edit` (append ops) < `Admin` (manage membership; revoke anyone).
85
+ - **Membership ops** (`Delegation` / `Revocation`) form a hash-linked DAG requiring only causal order — the same consistency model as Automerge. Non-admins may only revoke members they have _causal seniority_ over; admins may revoke anyone. Members may sub-delegate (attenuated) capabilities.
86
+ - **No global identity.** An `Individual` is one immutable Ed25519 key. A _person_ is modeled as a `Group` over their device keys; device add/remove is ordinary delegation/revocation. There is no built-in recovery — recovery keys are modeled as group members.
87
+ - **`ContactCard`**: a shareable signed credential (individual id + fresh prekey + signature) used to introduce identities and add them to groups/documents — including **while they are offline**, via published X25519 **prekeys**.
88
+
89
+ ### 2.2 Cryptography
90
+
91
+ - **BeeKEM** — a concurrent TreeKEM/CGKA variant. Binary tree: leaves are member keys, inner nodes carry DH public keys + encrypted path secrets; the root secret keys the document. Removal blanks the member's path (removal wins over concurrent updates); concurrent updates keep _conflict keys_ and encrypt to both forks' resolutions. Needs only causal ordering (no MLS-style sequencing server).
92
+ - **Primitives**: Ed25519 (signing/identity), X25519 (ECDH "share keys", rotating), BLAKE3 (KDF), XChaCha20-Poly1305 (AEAD).
93
+ - **PCS**: leaf rotations + blanking give post-compromise security (`force_pcs_update`); `PcsKey` derives from the tree root.
94
+ - **Causal encryption**: content is encrypted per change/chunk; each chunk embeds the keys of its causal ancestors, so one head key grants access back through history (deliberately trades forward secrecy at the content layer for late-joiner history access). `ApplicationSecret` = KDF(PcsKey, content ref).
95
+ - **Sedimentree**: hierarchical chunking of the encrypted commit graph so servers sync ciphertext efficiently (~2 round trips) without plaintext.
96
+
97
+ ### 2.3 Data flows
98
+
99
+ - **Membership propagation**: delegations/revocations/CGKA ops are signed events replicated as grow-only sets; per-peer visibility computed (`events_for_agent`); ingestion buffers events until causal dependencies arrive; set reconciliation via rateless IBLT (RIBLT).
100
+ - **Auth handshake**: every sync message is signed by a non-extractable local key and audience-bound (recipient key or hostname hash) + timestamped — no session handshake state.
101
+ - **Automerge integration**: Keyhive plugs into Automerge Repo as an _auth provider_; **Subduction** is the successor sync protocol with explicit Keyhive + Automerge integration (sedimentree chunking, E2EE-friendly). DXOS already vendors subduction-flavored automerge packages.
102
+ - **Persistence**: `Keyhive` state serializes via `Archive`; signer is pluggable (fits WebCrypto non-extractable keys — replacing our keyring's role).
103
+
104
+ ## 3. Concept mapping: HALO → Keyhive
105
+
106
+ | HALO (current) | Keyhive analogue | Notes |
107
+ | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
108
+ | Identity (identity key + HALO space) | `Group` over device keys ("identity group") | Identity DID derives from the group's key. The HALO space's _credential_ role dissolves into the group's membership DAG. |
109
+ | Device (device key + `AuthorizedDevice` chain) | `Individual` (leaf Ed25519 key) member of the identity group | Device admission = delegation into the identity group; no bespoke chain verification. |
110
+ | `AuthorizedDevice` credential | `Delegation` (identity group → device key) | |
111
+ | Device removal (not well supported today) | `Revocation` + BeeKEM path blanking | Gains PCS: removed devices lose access to future content. |
112
+ | Space (space key + control/data feeds) | `Document` (group + content) — or a `Group` owning many Automerge docs | Space membership = document/group membership. |
113
+ | `SpaceGenesis` | Document/group creation (self-certifying root) | |
114
+ | `SpaceMember` role: READER / EDITOR / ADMIN / OWNER | `Access`: `Read` / `Edit` / `Admin` (+ `Pull`) | OWNER ≈ root delegation holder. `Pull` is new: trust-minimized relays. REMOVED = revocation. |
115
+ | `AdmittedFeed` | — (obsolete) | Feeds disappear; Automerge/Subduction sync replaces hypercore replication. |
116
+ | Credential DAG (`parent_credential_ids`, `tryPickWinningUpdate`) | Membership op DAG (`auth_pred`, causal seniority rules) | Keyhive natively resolves concurrent membership changes. |
117
+ | Keyring (ECDSA-P256, exportable records) | Pluggable `AsyncSigner` + `ShareKeyMap` / prekeys | Ed25519/X25519; supports non-extractable WebCrypto keys. |
118
+ | Credential feed (hypercore, append-only) | Signed event sets (delegations/revocations/CGKA ops) + RIBLT sync | Causal DAG instead of per-feed total order. |
119
+ | Interactive invitation state machines | Out of band for Keyhive: `ContactCard` exchange + delegation | Swarm/QR/PIN UX stays ours (MESH); admission becomes "receive contact card → delegate access". Offline invitations come free via prekeys. |
120
+ | `DelegateSpaceInvitation` | Delegation to a not-yet-online individual (prekeys), or to an ephemeral invitation key that sub-delegates | Sub-delegation is a first-class Keyhive feature. |
121
+ | `IdentityProfile` / `MemberProfile` credentials | Application data (ECHO objects) — e.g. `Profile` in an identity-owned document | Profiles stop being credentials; they become replicated data governed by capabilities. |
122
+ | `DeviceProfile` | Application data (`Device` object) | Same. |
123
+ | Contacts (derived by `ContactsService`) | `AddressBook` of `Profile` objects + stored `ContactCard`s | Becomes explicit, persistent application data. |
124
+ | `IdentityRecovery` | Recovery key as member of the identity group | Delegation with high access; no special-case credential. |
125
+ | Replication auth (`TrustedKeySetAuthVerifier`) | Signed, audience-bound sync messages + `Pull` capability | Also gains E2EE — servers only ever see ciphertext. |
126
+ | `Presentation` / nonce challenge | Signed messages / capability `Invocation` | |
127
+ | `Epoch` | Sedimentree strata / compaction | Different mechanism, same GC intent. |
128
+
129
+ ## 4. Migration path (high level)
130
+
131
+ Phased; no backwards compatibility. Each phase lands behind the new `@dxos/halo` package so the legacy stack keeps working until cut-over.
132
+
133
+ 1. **Schema layer (this package).** Define Effect schemas for the durable, application-visible types — `Profile`, `Device`, `Group`, `AddressBook` — as ECHO object types addressable by EID, with identities/devices addressed by DID (`@dxos/keys`). These replace `ProfileDocument`, `DeviceProfileDocument`, and derived contacts, and are independent of the credential machinery.
134
+ 2. **Keyhive service.** Wrap `keyhive_wasm` in an Effect service (`Keyhive.Service`): identity-group creation, delegation/revocation, contact cards, prekeys, archive persistence. Adapt `@dxos/keyring` usage to the pluggable signer (Ed25519, ideally non-extractable WebCrypto).
135
+ 3. **Identity rewrite.** `createIdentity` → create identity `Group` + first device `Individual`; DID derived from the group key. Profile becomes a `Profile` object in an identity-owned document (HALO "space" becomes a plain Keyhive document group).
136
+ 4. **Device join.** Keep the MESH invitation UX (swarm, QR, auth code) as pure transport for `ContactCard` exchange; admission = delegation of the new device key into the identity group. Device removal = revocation (new capability we cannot express today without re-keying).
137
+ 5. **Space membership / access control.** Spaces become Keyhive `Document` groups over their Automerge docs (via Subduction, already vendored). Map roles to `Access` levels; delete `SpaceStateMachine`, `MemberStateMachine`, the credential DAG, and `AdmittedFeed` entirely — Keyhive's membership DAG subsumes them. Introduce `Pull`-level membership for edge/relay services.
138
+ 6. **Invitations.** Interactive invitations → contact-card exchange + immediate delegation. Delegated/offline invitations → prekey-based delegation or ephemeral invitation keys with attenuated sub-delegation. Delete `DelegateSpaceInvitation` credentials and the invitation state machine's credential plumbing.
139
+ 7. **Decommission.** Remove hypercore feeds, protobuf credential definitions, `@dxos/credentials` state machines, and the ECDSA keyring. Contacts UI reads `AddressBook`/`Profile` objects instead of `ContactsService`.
140
+
141
+ **Open questions / risks:**
142
+
143
+ - Keyhive is pre-alpha and unaudited; API churn is certain. Isolate it behind the `@dxos/halo` service boundary.
144
+ - Concurrent admin-vs-admin revocation semantics differ from HALO's OWNER-wins rule; product implications need review.
145
+ - Content-layer forward secrecy is deliberately absent in Keyhive (causal encryption); confirm this matches our threat model.
146
+ - Agent/server ("AGENT" device type) key custody with non-extractable keys needs a story (e.g. `Pull`-only relays + delegated agent keys).
147
+
148
+ ## 5. Prototype service and HALO shim feasibility
149
+
150
+ > Note: the pure-TypeScript Keyhive prototype (`Keyhive.ts`) has been **removed from
151
+ > `@dxos/halo`** to keep that package definitions-only (see `API_AUDIT.md` §3.5); its runtime is
152
+ > destined for a separate `@dxos/halo-keyhive`. The feasibility analysis below is retained as
153
+ > design rationale.
154
+
155
+ The prototype implemented a pure-TypeScript, in-memory model of the Keyhive membership layer
156
+ (Keyhive is not published to npm). Its `Keyhive.Service` interface mirrored the `keyhive_wasm`
157
+ bindings so a WASM-backed layer could replace `layerMemory()` unchanged.
158
+
159
+ **Can the existing HALO implementation be shimmed behind this API?**
160
+ Mostly yes for the membership core; no for encryption. A `layerLegacy()` backed by `@dxos/credentials` +
161
+ `@dxos/client-services` would map:
162
+
163
+ | Service operation | Legacy backing | Fidelity |
164
+ | -------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
165
+ | `active` | Device key (DID from device public key) | Good — devices are already key-identified principals. |
166
+ | `createGroup` | Space genesis (`SpaceGenesis` + `SpaceMember(OWNER)`) | Good — both are self-certifying roots. |
167
+ | `delegate` | `SpaceMember` credential with role mapped from `Access` | Partial — `admin→ADMIN`, `edit→EDITOR`, `read→READER`; **`pull` has no legacy equivalent** (no E2EE, membership implies readability) — reject. |
168
+ | `revoke` | `SpaceMember(REMOVED)` | Partial — legacy resolves conflicts via credential DAG + OWNER-wins, not causal seniority; semantics differ under concurrency. |
169
+ | `members` | `MemberStateMachine` output | Good — both are materialized views. |
170
+ | `contactCard` | Identity key + `ProfileDocument` (as a `Presentation`) | Partial — legacy has no prekeys, so offline admission needs `DelegateSpaceInvitation` instead of prekey delegation. |
171
+ | `receiveContactCard` | Verify presentation; record identity key | Good. |
172
+ | `ops` / `receiveOps` | Credential feed messages | Partial — legacy ops are totally ordered per feed (hypercore), not a causal DAG; export is possible, import must be replayed in feed order. |
173
+
174
+ Structural gaps that cannot be shimmed:
175
+
176
+ 1. **Nested groups.** Legacy members are identities only; a `delegate` whose subject is another
177
+ group/space has no credential representation. Shim must reject group subjects.
178
+ 2. **Attenuated sub-delegation.** Legacy `EDITOR`/`READER` cannot invite at all
179
+ (`_canInviteNewMembers` requires ADMIN/OWNER); Keyhive lets any member delegate up to their
180
+ own access. Shim must restrict delegation to admins, which is _stricter_ than the API implies.
181
+ 3. **Encryption surface.** Anything CGKA-related (`ApplicationSecret`, PCS rotation) is
182
+ unimplementable over plaintext feeds; the service deliberately excludes it so the membership
183
+ API stays shimmable.
184
+ 4. **Signer.** Legacy keyring is ECDSA-P256, prototype is Ed25519 — the op envelope carries the
185
+ algorithm implicitly via the author key, so a shim needs an algorithm tag or must reuse the
186
+ keyring's P-256 keys for signing ops.
187
+
188
+ Conclusion: a legacy shim is feasible as a _transition layer_ — same API, reduced semantics
189
+ (no `pull`, no nested groups, admin-only delegation, feed-ordered sync) — which would let
190
+ `client-services` code migrate to the `Keyhive.Service` API before the Keyhive/Subduction
191
+ backend lands. The prototype's in-memory layer and a future `layerLegacy()` are then two
192
+ implementations of one seam, and cut-over is a layer swap.
193
+
194
+ ### 5.1 EDGE service specification
195
+
196
+ How the EDGE service (`@dxos/edge-client`, `EdgeService` in `@dxos/protocols`) participates in
197
+ the Keyhive model — both under the legacy shim and after cut-over.
198
+
199
+ **Identity.** Each EDGE deployment is a first-class principal: an Ed25519 key with a DID, like
200
+ any device. The current `EdgeIdentity` challenge/`Presentation` handshake (`presentCredentials`
201
+ with nonce, `/ws/<identityDid>/<peerKey>`) is replaced by Keyhive/Subduction message auth:
202
+ every message signed by the sender's key and audience-bound to the EDGE hostname hash
203
+ (anti-MITM, anti-replay) — no session handshake state.
204
+
205
+ **Admission.** EDGE is admitted to a space/group like any member:
206
+ `delegate({ group, subject: <edge did>, access: 'pull' })`. This makes relay access explicit,
207
+ auditable, and revocable in the membership DAG — today EDGE access is implicit in feed
208
+ replication auth. Server-side capabilities that require plaintext (AI, search/indexing) must be
209
+ admitted as explicit `read`-level members, making that trust decision visible per space rather
210
+ than ambient.
211
+
212
+ **Capabilities at `pull` level.** EDGE may: store and forward ciphertext (the `CiphertextStore`
213
+ role; Sedimentree strata + commit-graph hashes, which stay outside the encryption envelope for
214
+ sync); relay membership ops (delegations/revocations/CGKA ops are signed, self-verifying
215
+ events); serve set-reconciliation (RIBLT) exchanges. EDGE cannot: decrypt content, author or
216
+ forge membership ops, or build plaintext indexes.
217
+
218
+ **Service mapping** (`EdgeService` enum):
219
+
220
+ | Service | Disposition under Keyhive |
221
+ | ----------------------- | --------------------------------------------------------------------------------------- |
222
+ | `SUBDUCTION_REPLICATOR` | Primary sync path; EDGE acts as a `pull`-level Subduction peer. |
223
+ | `AUTOMERGE_REPLICATOR` | Superseded by Subduction; retire with the migration. |
224
+ | `FEED_REPLICATOR` | Deleted — already `TODO(mykola): Remove once we migrate to keyhive for access control`. |
225
+ | `QUEUE_REPLICATOR` | Retained (renamed per existing TODO); queue payloads become ciphertext under `pull`. |
226
+ | `SWARM` / `SIGNAL` | Unchanged — pure transport; requires message auth but no group capability. |
227
+ | `STATUS` | Unchanged. |
228
+
229
+ **Hub duties layered on EDGE** (require no `read` access):
230
+
231
+ 1. **Offline invitation mailbox.** Prekey-based admission means delegations can be issued for
232
+ offline invitees; EDGE parks the signed ops (and encrypted welcome payloads) until the
233
+ invitee connects — replacing `DelegateSpaceInvitation` availability semantics.
234
+ 2. **Contact directory.** Handle/email → DID + `ContactCard` resolution (comparable to atproto
235
+ handle resolution). Contact cards are signed and self-verifying, so the directory is
236
+ untrusted for integrity — it can withhold but not forge.
237
+ 3. **Agent key custody.** `agent-managed` devices become distinct principals whose access is
238
+ explicitly delegated (`read`/`edit` as needed) rather than inherited from feed replication;
239
+ custody of those keys is an EDGE responsibility and revocation is an ordinary membership op.
240
+
241
+ **Under the legacy shim.** `layerLegacy()` cannot grant `pull` (feeds are plaintext), so EDGE
242
+ remains read-equivalent until the Keyhive backend lands. The shim-era spec is therefore:
243
+ adopt the delegation-based admission and signed message auth now (both expressible over
244
+ credentials), and treat `pull` enforcement as activated by the backend swap, not by an EDGE
245
+ API change.
246
+
247
+ ## A. ISSUES
248
+
249
+ 1. Migration from protobuf?
250
+ 2. Alt:
251
+ - a. Old API => Old Hypercore impl.
252
+ - b. New API => Old Hypercore impl.
253
+ - c. New API => New EDGE endpoint impl (shim).
254
+ - d. New API => Keyhive service impl.
@@ -0,0 +1,217 @@
1
+ # HALO consumer migration — plan & decisions
2
+
3
+ Working plan for migrating Composer/plugins off direct `@dxos/client` HALO access
4
+ onto the `@dxos/halo` Effect services. Tracks the settled preferences so scope
5
+ does not need to be re-litigated.
6
+
7
+ ## Settled preferences (do not re-ask)
8
+
9
+ - **One big PR.** Land as much of the migration as cleanly possible in a single
10
+ PR (#12229) on branch `claude/halo-api-audit-migration-w5al1i`, rather than a
11
+ string of small follow-ups. The one acknowledged exception is the deferred
12
+ service verbs (see Phase 2 / Status below): the consumers that depend on them
13
+ cannot migrate until those verbs exist, so they remain outstanding regardless
14
+ of PR count.
15
+ - **Extend the HALO API as needed.** Where a consumer needs something the
16
+ DID-only `Identity.Info` dropped, extend the HALO service/adapter rather than
17
+ leaving the consumer on `@dxos/client`. Planned extensions:
18
+ - `Identity.Info`: add `identityKey` (hex) back, and `profile` data
19
+ (`{ displayName?, data? }`) so Profile UI (emoji/hue) works.
20
+ - `Identity.updateProfile`: accept `{ displayName?, data? }`.
21
+ - `Identity`: add `updateDevice`, credential query/write, recovery-credential
22
+ creation (`createRecoveryCredential` / `requestRecoveryChallenge`), and EDGE
23
+ attest (`createEdgeIdentity`/`presentCredentials`) verbs.
24
+ - `Identity.create`: expose the personal `spaceId` (or add `Space.personal`).
25
+ - `useCredentials` hook in `@dxos/halo-react`.
26
+ - **ECHO stays separate.** `space.db` / `useQuery` / properties remain on
27
+ `@dxos/echo` / `@dxos/react-client/echo` — that is the ECHO track, not
28
+ `@dxos/client` HALO. `useSpaces`/`useSpace` sites that exist only to reach
29
+ `space.db` are NOT migrated to `@dxos/halo-react`.
30
+ - **Mutations route through Operations** (app-framework), handlers use the
31
+ services; components dispatch operations.
32
+
33
+ ## Known external blocker (not fixable from here)
34
+
35
+ CI `check-packages-published` requires `@dxos/halo`, `@dxos/halo-adapter-client`,
36
+ `@dxos/halo-react` to be published to npm with OIDC trusted publishing. That is a
37
+ maintainer/release action. The PR stays red on that one check until a maintainer
38
+ publishes the packages; all other checks should be green.
39
+
40
+ ## Phases (all in the one PR)
41
+
42
+ 1. **plugin-client foundation** — DONE: `IdentityLayerSpec`/`SpaceLayerSpec`,
43
+ `HaloProvider`, deps. Packages made publishable.
44
+ 2. **Extend HALO API + adapter** — add the verbs/fields above to `@dxos/halo`
45
+ and implement in `@dxos/halo-adapter-client`; add `useCredentials` and any
46
+ missing hooks to `@dxos/halo-react`.
47
+ 3. **plugin-client operations** — `create-identity`, `update-profile` (new),
48
+ recovery/passkey, device update: declare + use `Identity.Service`.
49
+ 4. **plugin-client containers** — Account (DONE), Profile, Devices, Recovery.
50
+ 5. **Other plugins** — swap `useIdentity`/`useDevices`/`useMembers`/
51
+ space-invitation hooks to `@dxos/halo-react`; leave ECHO `space.db` sites.
52
+ 6. **Cleanup** — drop now-unused `@dxos/react-client/halo` imports; changeset.
53
+
54
+ ## Migration outcome — what moved and what stays
55
+
56
+ The migration decision is: **plugins consume HALO via `@dxos/halo` /
57
+ `@dxos/halo-react`, never `@dxos/client` HALO APIs.** ECHO `space.db` stays on
58
+ `@dxos/echo`. The only place the client backs HALO is the adapter
59
+ (`@dxos/halo-adapter-client`) and plugin-client's client provider + the
60
+ `IdentityService`/`SpaceService` capabilities it contributes.
61
+
62
+ ### HALO API added to support consumers
63
+
64
+ - `Identity.Info` gained `identityKey` (hex) + `data`; `Space.Member` gained
65
+ `identityKey` (hex), `displayName`, `data`.
66
+ - `Identity`: `credentials` stream + `Credential` type, `grantServiceAccess`
67
+ verb, synchronous `getSnapshot()` / `getDevicesSnapshot()` and imperative
68
+ `subscribe()` for non-React/non-Effect callers.
69
+ - `@dxos/halo-react`: `useCredentials` (plus existing useIdentity/useDevices/
70
+ useMembers/useSpaces/useInvitations).
71
+ - plugin-client contributes `ClientCapabilities.IdentityService` / `SpaceService`
72
+ (the service instances) for imperative capability singletons.
73
+
74
+ ### Migrated (off `@dxos/client` HALO)
75
+
76
+ - **React consumers** — plugin-client (Account/Profile/Recovery containers,
77
+ UpdateProfile operation), plugin-assistant (Chat/ChatThread),
78
+ plugin-comments (CommentsArticle, CommentThread + `getMessageMetadata`),
79
+ plugin-transcription (useTranscriptionRecording, TranscriptionArticle),
80
+ plugin-markdown / plugin-code / plugin-script editors, plugin-thread
81
+ (MessageThread/ThreadArticle/ChannelArticle), plugin-space (SpacePresence).
82
+ Enabled by narrowing shared UI signatures to structural types
83
+ (`DataExtensionsIdentity` in `@dxos/ui-editor`, `BylineIdentity` in
84
+ `@dxos/react-ui-transcription`, `MessageAuthor` in thread/comments) that the
85
+ HALO `Identity.Info` / `Space.Member` satisfy — no `@dxos/halo` dependency
86
+ pushed into foundational UI packages.
87
+ - **Imperative capability singletons** — plugin-iroh-beacon (beacon-service),
88
+ plugin-assistant (edge-model-resolver identity reads), plugin-calls
89
+ (call-manager + CallSwarmSynchronizer), plugin-meeting (call-extension),
90
+ plugin-space (navigation-handler, spaces-ready, join operation),
91
+ plugin-transcription (transcriber) — all read identity/devices via the
92
+ `IdentityService` capability (`getSnapshot`/`getDevicesSnapshot`/`subscribe`).
93
+
94
+ ## Missing APIs
95
+
96
+ The end goal (per maintainer direction) is to **eliminate all `@dxos/client` and
97
+ `@dxos/react-client` imports from `packages/plugins/*` and `packages/ui/*`**. The
98
+ following APIs do not yet exist and block that; every plugin line below that is
99
+ not "complete" references one of these numbers.
100
+
101
+ ### HALO (this PR's surface)
102
+
103
+ 1. **Personal space id from identity creation.** `Identity.create` must return
104
+ the personal `spaceId` (or add `Identity.personalSpaceId` / `Space.personal`).
105
+ Today consumers read `client.halo.identity.get()?.spaceKey`.
106
+ 2. **Recovery-credential creation.** `Identity.createRecoveryCredential({ recoveryKey,
107
+ algorithm, lookupKey })` (+ `requestRecoveryChallenge`). The WebAuthn/passkey
108
+ ceremony stays at the call site; only the credential write moves to HALO.
109
+ 3. **EDGE identity / VP-auth verb.** A HALO replacement for
110
+ `createEdgeIdentity(client)` (`@dxos/client/edge`) + `presentCredentials`, so a
111
+ consumer can attach the signed-in identity to an EDGE/Hub HTTP client.
112
+ 4. **Device-invitation share for non-client UI.** `Identity.share()` must surface a
113
+ flow/observable the UI can render without `@dxos/client/invitations`, plus a
114
+ HALO-typed device that `@dxos/shell`'s `DeviceListItem` accepts (or a
115
+ halo-typed list item). Today: `client.halo.share()` + client `Device`.
116
+ 5. **Operation/hook to invoke `grantServiceAccess` from React.** The verb exists;
117
+ a `ClientOperation` (or `useGrantServiceAccess`) is needed so components can
118
+ call it without `client.halo.writeCredentials`.
119
+ 6. **Identity atom for graph builders.** An `Atom`/observable bridge (or documented
120
+ `Atom.make(Identity.identity…)` recipe) to replace
121
+ `CreateAtom.fromObservable(client.halo.identity)` in app-graph builders.
122
+
123
+ ### ECHO track (separate, much larger — ~340 imports)
124
+
125
+ 7. **`@dxos/echo` / `@dxos/echo-client` React bindings** replacing
126
+ `@dxos/react-client/echo` (and `@dxos/client/echo`): `useQuery`, `useSpace`,
127
+ `useSpaces`, `useObject`, `getSpace`, and the `Space` / `SpaceMember` value
128
+ types. This is the single biggest blocker to dropping `@dxos/react-client`.
129
+ 8. **Space-invitation UI via HALO.** Migrate `@dxos/react-client/invitations` +
130
+ `@dxos/client/invitations` UIs onto `Space.share` / `Space.join` /
131
+ `Space.invitations` (the verbs exist; the React/observable surface does not).
132
+
133
+ ### Platform / root client
134
+
135
+ 9. **Config / mesh / services access** without the `@dxos/client` root export or
136
+ `useClient` — e.g. `client.config`, `client.mesh.networkStatus`,
137
+ `client.services`. Needed by app-graph-builder, DevicesContainer (network
138
+ status), plugin-debug, and every `useClient()`/`Client`/`Config` site.
139
+ 10. **Test harness.** A replacement for `@dxos/react-client/testing`
140
+ (`withClientProvider`, `useClientStory`) used by plugin stories/tests. Low
141
+ priority; unblocks the last imports after 7–9 land.
142
+
143
+ ## Composer plugins
144
+
145
+ HALO column: status of _this_ migration (HALO off `@dxos/client`). Blockers:
146
+ what still pins `@dxos/client` / `@dxos/react-client`, by Missing-API number
147
+ above. **Any plugin not listed here imports neither `@dxos/client` nor
148
+ `@dxos/react-client` in non-test/story source — complete.** `(story)` = the only
149
+ remaining import is in a `*.stories.tsx`.
150
+
151
+ | Plugin | HALO | Remaining `@dxos/client` / `@dxos/react-client` pins |
152
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
153
+ | plugin-client | partial | create-identity (1), create-passkey (2), useHubClient (3), DevicesContainer (4, 9), app-graph-builder (6, 9), invitations UI (8), the client provider + `IdentityService`/`SpaceService` capabilities (intentional boundary) |
154
+ | plugin-onboarding | partial | WelcomeScreen/onboarding-manager/util/oauth (1, 2), ECHO (7) |
155
+ | plugin-script | partial | react-surface credential write (5), deploy/functions helpers take a `client` param, EDGE (3), ECHO (7) |
156
+ | plugin-assistant | complete | edge-model-resolver EDGE identity (3), ECHO (7) |
157
+ | plugin-connector | n/a | EDGE (3), ECHO (7) |
158
+ | plugin-payments | n/a | EDGE (3) |
159
+ | plugin-comments | complete | ECHO (7) |
160
+ | plugin-thread | complete | ECHO (7); `useIdentity` (story) |
161
+ | plugin-space | complete | ECHO (7), invitations (8), config/mesh (9) |
162
+ | plugin-transcription | complete | ECHO (7) |
163
+ | plugin-markdown | complete | ECHO (7) |
164
+ | plugin-code | complete | ECHO (7) |
165
+ | plugin-calls | complete | config/services (9) |
166
+ | plugin-meeting | complete | ECHO (7) |
167
+ | plugin-iroh-beacon | complete | — (off client) |
168
+ | plugin-observability | n/a | config/telemetry (9) |
169
+ | plugin-registry | n/a | CLI commands construct the client |
170
+ | plugin-debug, plugin-devtools | n/a | mesh/devtools (9), ECHO (7) |
171
+ | plugin-space (CLI), plugin-client (CLI) | n/a | `src/commands/**` construct the client — separate execution model |
172
+ | plugin-board, -bookmarks, -chess, -chess-com, -conductor, -crm, -explorer, -file, -freeq, -heygen, -ibkr, -inbox, -kanban, -magazine, -map, -outliner, -preview, -sample, -search, -sequencer, -sheet, -spacetime, -stack, -status-bar, -studio, -support, -table, -trip, -video, -wnfs, -zen, -bluesky, -commerce, -pipeline, -routine, -native-filesystem, -sandbox, -doctor | n/a | ECHO (7), and `/testing` (10) where present — no HALO usage |
173
+
174
+ ## `@dxos/client` / `@dxos/react-client` usage inventory (snapshot)
175
+
176
+ Import counts across `packages/plugins/*/src` + `packages/ui/*/src`
177
+ (non-test/story), to size the elimination effort:
178
+
179
+ | Import | Count | Track |
180
+ | -------------------------------- | ----- | --------------------------------------------- |
181
+ | `@dxos/react-client/echo` | 288 | ECHO (7) |
182
+ | `@dxos/client` (root) | 102 | platform (9) |
183
+ | `@dxos/react-client/testing` | 67 | test harness (10) |
184
+ | `@dxos/react-client` (root) | 64 | platform (9) |
185
+ | `@dxos/client/echo` | 52 | ECHO (7) |
186
+ | `@dxos/react-client/halo` | 8 | HALO — plugin-client + plugin-onboarding only |
187
+ | `@dxos/client/invitations` | 7 | invitations (8) |
188
+ | `@dxos/client/edge` | 6 | EDGE (3) |
189
+ | `@dxos/react-client/invitations` | 4 | invitations (8) |
190
+ | `@dxos/react-client/mesh` | 3 | platform (9) |
191
+ | `@dxos/client/halo` | 3 | HALO — plugin-client + plugin-onboarding |
192
+ | `@dxos/react-client/devtools` | 1 | devtools (9) |
193
+ | `@dxos/client/testing` | 1 | test harness (10) |
194
+
195
+ **`react-ui*` packages still importing the client** (all ECHO/root, none HALO
196
+ after this PR): `@dxos/client` → `react-ui-editor`, `react-ui-masonry`,
197
+ `ui-editor`; `@dxos/react-client` → `react-ui-canvas-compute`,
198
+ `react-ui-canvas-editor`, `react-ui-chat`, `react-ui-components`,
199
+ `react-ui-editor`, `react-ui-form`, `react-ui-markdown`, `react-ui-masonry`,
200
+ `react-ui-mosaic`, `react-ui-table`, `react-ui-transcription`. These block on
201
+ Missing API 7 (ECHO React bindings), not on HALO.
202
+
203
+ ## Status
204
+
205
+ - **HALO React consumer tier: complete.**
206
+ - **HALO imperative singleton tier: complete** except the EDGE-auth (3) and
207
+ script/onboarding cases.
208
+ - **HALO remaining tier: outstanding** — Missing APIs 1–6 (identity-creation
209
+ verbs, recovery credential, EDGE verb, device-invitation/Shell, script write-op,
210
+ graph-builder atom). Only `plugin-client`, `plugin-onboarding`, `plugin-script`,
211
+ and the EDGE consumers (`plugin-assistant`, `plugin-connector`, `plugin-payments`)
212
+ still touch the client for HALO/EDGE. Tracked as task #7.
213
+ - **Full `@dxos/client`/`@dxos/react-client` elimination: future scope** — gated on
214
+ Missing APIs 7–10 (ECHO React bindings dominate at ~340 imports), tracked
215
+ separately from this HALO PR.
216
+ - **External blocker unchanged**: `check-packages-published` stays red until a
217
+ maintainer publishes the three `@dxos/halo*` packages + OIDC.
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # @dxos/halo
2
+
3
+ DXOS HALO
4
+
5
+ - [MIGRATION.md](./MIGRATION.md) — HALO → Keyhive migration plan.
6
+ - [API_AUDIT.md](./API_AUDIT.md) — audit of HALO API usage across Composer/plugins and the proposed Effect service replacement.
7
+ - [CONSUMER_MIGRATION.md](./CONSUMER_MIGRATION.md) — cookbook for moving consumers off `@dxos/client` onto the HALO services.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pnpm i @dxos/halo
13
+ ```
14
+
15
+ ## DXOS Resources
16
+
17
+ - [Website](https://dxos.org)
18
+ - [Developer Documentation](https://docs.dxos.org)
19
+ - Talk to us on [Discord](https://dxos.org/discord)
20
+
21
+ ## Contributions
22
+
23
+ Your ideas, issues, and code are most welcome. Please take a look at our [community code of conduct](https://github.com/dxos/dxos/blob/main/CODE_OF_CONDUCT.md), the [issue guide](https://github.com/dxos/dxos/blob/main/CONTRIBUTING.md#submitting-issues), and the [PR contribution guide](https://github.com/dxos/dxos/blob/main/CONTRIBUTING.md#submitting-prs).
24
+
25
+ License: [FSL-1.1-Apache-2.0](./LICENSE) Copyright 2026 © DXOS