@pollen-robotics/reachy-mini-sdk 1.8.0 → 1.8.1
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/APP_CREATION_GUIDE.md +1795 -0
- package/README.md +2 -5
- package/dist/animation/distance.d.ts +87 -0
- package/dist/animation/distance.d.ts.map +1 -0
- package/dist/animation/distance.js +140 -0
- package/dist/animation/distance.js.map +1 -0
- package/dist/animation/index.d.ts +34 -0
- package/dist/animation/index.d.ts.map +1 -0
- package/dist/animation/index.js +33 -0
- package/dist/animation/index.js.map +1 -0
- package/dist/animation/pose.d.ts +40 -0
- package/dist/animation/pose.d.ts.map +1 -0
- package/dist/animation/pose.js +15 -0
- package/dist/animation/pose.js.map +1 -0
- package/dist/animation/presets.d.ts +85 -0
- package/dist/animation/presets.d.ts.map +1 -0
- package/dist/animation/presets.js +82 -0
- package/dist/animation/presets.js.map +1 -0
- package/dist/animation/safe-return.d.ts +90 -0
- package/dist/animation/safe-return.d.ts.map +1 -0
- package/dist/animation/safe-return.js +123 -0
- package/dist/animation/safe-return.js.map +1 -0
- package/dist/lib/reachy-mini.d.ts +99 -0
- package/dist/lib/reachy-mini.d.ts.map +1 -1
- package/dist/lib/reachy-mini.js +314 -9
- package/dist/lib/reachy-mini.js.map +1 -1
- package/dist/lib/types.d.ts +27 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/host/README.md +14 -12
- package/host/dist/ReachyHost.d.ts +2 -1
- package/host/dist/ReachyHost.d.ts.map +1 -1
- package/host/dist/chunks/index-lMs2sCXG.js +439 -0
- package/host/dist/chunks/{mountHost-D7cgkU9Q.js → mountHost-DjE5zE9R.js} +11142 -11014
- package/host/dist/chunks/{reachy-mini-B1hlBmDQ.js → reachy-mini-GHNS4GSp.js} +494 -307
- package/host/dist/components/PickerView.d.ts.map +1 -1
- package/host/dist/components/PostOAuthSplash.d.ts +25 -0
- package/host/dist/components/PostOAuthSplash.d.ts.map +1 -0
- package/host/dist/components/ReachyHostShell.d.ts.map +1 -1
- package/host/dist/components/TopBar.d.ts +8 -1
- package/host/dist/components/TopBar.d.ts.map +1 -1
- package/host/dist/components/WelcomeBackOverlay.d.ts.map +1 -1
- package/host/dist/embed/index.d.ts +3 -2
- package/host/dist/embed/index.d.ts.map +1 -1
- package/host/dist/entry/auto.js +2 -2
- package/host/dist/entry/embed.js +2 -2
- package/host/dist/hooks/useHostBridge.d.ts +6 -0
- package/host/dist/hooks/useHostBridge.d.ts.map +1 -1
- package/host/dist/hooks/useSdk.d.ts.map +1 -1
- package/host/dist/index.js +3 -3
- package/host/dist/lib/protocol.d.ts +17 -3
- package/host/dist/lib/protocol.d.ts.map +1 -1
- package/host/dist/lib/settings.d.ts +3 -2
- package/host/dist/lib/settings.d.ts.map +1 -1
- package/host/dist/mountHost.d.ts.map +1 -1
- package/host/dist/ui/design/IdentityChipBar.d.ts +46 -0
- package/host/dist/ui/design/IdentityChipBar.d.ts.map +1 -0
- package/host/dist/ui/design/LinkQualityBars.d.ts +32 -0
- package/host/dist/ui/design/LinkQualityBars.d.ts.map +1 -0
- package/host/dist/ui/design/MetaPill.d.ts +41 -0
- package/host/dist/ui/design/MetaPill.d.ts.map +1 -0
- package/host/dist/ui/design/TransportChip.d.ts +39 -0
- package/host/dist/ui/design/TransportChip.d.ts.map +1 -0
- package/package.json +7 -4
- package/CHANGELOG.md +0 -188
- package/host/APP_AUTHOR_GUIDE.md +0 -646
- package/host/SPEC.md +0 -618
- package/host/dist/chunks/index-CyLPysJS.js +0 -400
package/host/SPEC.md
DELETED
|
@@ -1,618 +0,0 @@
|
|
|
1
|
-
# Reachy Mini app boot spec
|
|
2
|
-
|
|
3
|
-
| Spec version | Status | Last updated |
|
|
4
|
-
|--------------|------------|--------------|
|
|
5
|
-
| 1.0 | Active | 2026-05-16 |
|
|
6
|
-
|
|
7
|
-
Source-of-truth for what a Reachy Mini app must do at boot, what
|
|
8
|
-
`@pollen-robotics/reachy-mini-sdk/host` does for it, and the contract between the
|
|
9
|
-
shell, the embed and the app code.
|
|
10
|
-
|
|
11
|
-
This file describes **observable behaviour and invariants**. The
|
|
12
|
-
wire format lives in [`src/lib/protocol.ts`](./src/lib/protocol.ts).
|
|
13
|
-
The README is a tour for first-time integrators.
|
|
14
|
-
|
|
15
|
-
> **Read this in 15 minutes**: §1-§5 are the integration contract,
|
|
16
|
-
> §6 is the protocol, §7 is two state diagrams, §8 lists the
|
|
17
|
-
> non-negotiable invariants. §9-§11 are appendices.
|
|
18
|
-
|
|
19
|
-
## Table of contents
|
|
20
|
-
|
|
21
|
-
1. [Roles](#1-roles)
|
|
22
|
-
2. [Two boot modes, one URL surface](#2-two-boot-modes-one-url-surface)
|
|
23
|
-
3. [Mode A: Hub standalone flow](#3-mode-a-hub-standalone-flow)
|
|
24
|
-
4. [Mode B: Mobile handoff flow](#4-mode-b-mobile-handoff-flow)
|
|
25
|
-
5. [App-author contract](#5-app-author-contract)
|
|
26
|
-
6. [Protocol v1](#6-protocol-v1)
|
|
27
|
-
7. [State machines](#7-state-machines)
|
|
28
|
-
8. [Engineering invariants](#8-engineering-invariants)
|
|
29
|
-
9. [Non-goals](#9-non-goals)
|
|
30
|
-
10. [Threat model](#10-threat-model)
|
|
31
|
-
11. [Backlog](#11-backlog)
|
|
32
|
-
|
|
33
|
-
## How to evolve this spec
|
|
34
|
-
|
|
35
|
-
- §1-§7 describe the **current implementation**: update them as the
|
|
36
|
-
code changes, in the same PR.
|
|
37
|
-
- §8 lists **hard invariants**: every PR that touches the bridge or
|
|
38
|
-
the FSM must keep them green or update them explicitly with a
|
|
39
|
-
CHANGELOG entry.
|
|
40
|
-
- §11 is the **backlog**: graduate items into §8 only when they are
|
|
41
|
-
implemented and the cost of regressing them is real.
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## 1. Roles
|
|
46
|
-
|
|
47
|
-
Three actors, one app repository:
|
|
48
|
-
|
|
49
|
-
| Actor | Lives in | Owns |
|
|
50
|
-
|------------|-----------------------------------|----------------------------------------------------------------------------|
|
|
51
|
-
| **App** | `index.html` + `src/dispatch.ts` | UI, app-specific UX, **and full freedom over framework / tooling choices** |
|
|
52
|
-
| **Host** | `@pollen-robotics/reachy-mini-sdk/host/auto` (CDN) | OAuth, robot discovery, robot picker, connecting overlay, end-session flow |
|
|
53
|
-
| **Embed** | `@pollen-robotics/reachy-mini-sdk/host/embed` (CDN) | SDK lifecycle inside the iframe (`startSession`, `ensureAwake`, teardown) |
|
|
54
|
-
|
|
55
|
-
The **App** consumes:
|
|
56
|
-
|
|
57
|
-
- the Reachy Mini SDK (loaded as a CDN module by `index.html`,
|
|
58
|
-
exposed as `window.ReachyMini`),
|
|
59
|
-
- the `@pollen-robotics/reachy-mini-sdk/host` package (npm dep for types, CDN entries
|
|
60
|
-
at runtime).
|
|
61
|
-
|
|
62
|
-
The App contains zero auth code, zero picker code, zero
|
|
63
|
-
session-lifecycle code.
|
|
64
|
-
|
|
65
|
-
### App identity: `owner/space`
|
|
66
|
-
|
|
67
|
-
A Reachy Mini app is **uniquely identified by its Hugging Face
|
|
68
|
-
Space ID**, of the form `owner/space` (e.g. `pollen-robotics/emotions`).
|
|
69
|
-
Everything downstream of identity flows from this single string:
|
|
70
|
-
|
|
71
|
-
- The catalog API filters and dedupes apps by `space.id`.
|
|
72
|
-
- The mobile app stores and recalls "last opened" apps by `owner/space`.
|
|
73
|
-
- The host shell does **not** need this ID at runtime (it lives
|
|
74
|
-
inside the app it renders); it's used solely by the discovery
|
|
75
|
-
surface (catalog API + mobile).
|
|
76
|
-
|
|
77
|
-
### Official vs community apps
|
|
78
|
-
|
|
79
|
-
An app is **official** if and only if its Space ID starts with
|
|
80
|
-
`pollen-robotics/`. There is no allowlist, no separate registry,
|
|
81
|
-
no `official: true` field anywhere. Adding `pollen-robotics/` to
|
|
82
|
-
your URL is the entire qualification.
|
|
83
|
-
|
|
84
|
-
Where the distinction surfaces:
|
|
85
|
-
|
|
86
|
-
| Surface | Behaviour |
|
|
87
|
-
|----------------------|-----------------------------------------------------------------|
|
|
88
|
-
| Mobile catalog | "Official" badge / sort priority on `pollen-robotics/*` Spaces |
|
|
89
|
-
| Website `/api/js-apps` | Returns `isOfficial: true` for `pollen-robotics/*` |
|
|
90
|
-
| Host shell | **No notion of "official"**. Renders the app the same way always |
|
|
91
|
-
|
|
92
|
-
This is a non-decision at the host level: the host doesn't know,
|
|
93
|
-
doesn't care. Keep it that way.
|
|
94
|
-
|
|
95
|
-
### Tech freedom is a core design principle
|
|
96
|
-
|
|
97
|
-
The iframe boundary is intentional: it lets an app pick **any
|
|
98
|
-
framework, any bundler, any styling system** without the host
|
|
99
|
-
caring. The reference apps prove this in practice:
|
|
100
|
-
|
|
101
|
-
| Reference app | Stack | Demonstrates |
|
|
102
|
-
|-------------------------------------|-----------------------------|-----------------------------------------------|
|
|
103
|
-
| `reachy_mini_emotions` | React 19 + MUI 7 + Vite | A typed React app with rich UI components |
|
|
104
|
-
| `reachy_mini_telepresence` | React 19 + MUI 7 + Vite | An app that uses media streams + camera UI |
|
|
105
|
-
| `reachy_mini_minimal_conversation` | **Vanilla TS + Vite** | Zero-framework app, ~few KB runtime overhead |
|
|
106
|
-
|
|
107
|
-
If you add a new app, you do **not** need to consult the host
|
|
108
|
-
team about your stack. The only contract is: render in the
|
|
109
|
-
iframe, talk to the host through `connectToHost()`, ship the 4
|
|
110
|
-
files in §5.
|
|
111
|
-
|
|
112
|
-
### Why React + MUI for the host (and only the host)
|
|
113
|
-
|
|
114
|
-
The host shell (`@pollen-robotics/reachy-mini-sdk/host/auto`) is built with **React +
|
|
115
|
-
MUI + Emotion**. This is an explicit, assumed choice:
|
|
116
|
-
|
|
117
|
-
- The shell needs a real component library: forms (sign-in),
|
|
118
|
-
lists (picker), overlays (connecting / leaving), top bar, dark
|
|
119
|
-
mode toggles. Hand-rolling these in vanilla would slow us down
|
|
120
|
-
with no benefit to app authors.
|
|
121
|
-
- The shell renders only **outside the iframe** and only between
|
|
122
|
-
sessions; once an app is live, the shell's bundle is idle.
|
|
123
|
-
- App authors who chose a different framework still load the
|
|
124
|
-
shell React+MUI bundle for sign-in/picker UI. This is the cost
|
|
125
|
-
of the iframe model and we accept it (see §9 Non-goals).
|
|
126
|
-
|
|
127
|
-
The trade-off favours **fast host iteration + tech freedom for
|
|
128
|
-
apps** over a slimmer host shell. A vanilla micro-shell remains
|
|
129
|
-
possible if usage data ever justifies it (§11 backlog).
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
## 2. Two boot modes, one URL surface
|
|
134
|
-
|
|
135
|
-
The same `index.html` is served for both modes. The dispatcher
|
|
136
|
-
(`src/dispatch.ts`) picks between them based on the URL.
|
|
137
|
-
|
|
138
|
-
| Mode | URL shape | What happens |
|
|
139
|
-
|-----------------------|----------------------------------------------------------------------|------------------------------------------------------|
|
|
140
|
-
| **A. Hub standalone** | `https://<space>.hf.space/` | Full host shell (OAuth → picker → iframe with app) |
|
|
141
|
-
| **B. Mobile handoff** | `https://<space>.hf.space/?embedded=1#creds=<base64(CredsBundle)>` | Skip shell; app boots directly, creds come via hash |
|
|
142
|
-
|
|
143
|
-
### Dispatch rule
|
|
144
|
-
|
|
145
|
-
```
|
|
146
|
-
if (URL.searchParams.has("embedded") && URL.hash.startsWith("#creds=")):
|
|
147
|
-
boot embed -> import("./embed")
|
|
148
|
-
else:
|
|
149
|
-
boot host -> import("@pollen-robotics/reachy-mini-sdk/host/auto").mountHost({...})
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
`?embedded=1` without creds is an invalid mode - render `ErrorView`.
|
|
153
|
-
|
|
154
|
-
### `CredsBundle` (lives only in the URL hash, never in search)
|
|
155
|
-
|
|
156
|
-
The full shape lives in `src/lib/protocol.ts` (`CredsBundle`).
|
|
157
|
-
Key field: `hfToken` is a short-lived HF bearer (see §8.3). The
|
|
158
|
-
hash is never sent to a server; the embed wipes it with
|
|
159
|
-
`history.replaceState` on the very first synchronous tick, before
|
|
160
|
-
any `await`.
|
|
161
|
-
|
|
162
|
-
---
|
|
163
|
-
|
|
164
|
-
## 3. Mode A: Hub standalone flow
|
|
165
|
-
|
|
166
|
-
The user lands on `https://<space>.hf.space/` from anywhere
|
|
167
|
-
(direct link, HF Space gallery, etc.).
|
|
168
|
-
|
|
169
|
-
### 3.1 Boot → auth → picker
|
|
170
|
-
|
|
171
|
-
Phase machine inside `useReachyHost`:
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
booting → (signed-out | authenticated) → connecting → connected → picking
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
- **`booting`**: wait for `window.ReachyMini`, instantiate the SDK,
|
|
178
|
-
call `instance.authenticate()`.
|
|
179
|
-
- **`signed-out`**: render `SignInView` with a "Continue with
|
|
180
|
-
Hugging Face" button that starts an OAuth redirect.
|
|
181
|
-
- **`authenticated`** → `connecting` → `connected` (SSE welcome) →
|
|
182
|
-
`picking`.
|
|
183
|
-
|
|
184
|
-
During `picking`, `RobotPickerView` listens to the SDK's
|
|
185
|
-
`robotsChanged` event so robots appearing / disappearing / going
|
|
186
|
-
busy are reflected live.
|
|
187
|
-
|
|
188
|
-
The picker auto-picks only when `instance.preselectedRobotId` is
|
|
189
|
-
set (Mode B only). In standalone mode we always show the picker,
|
|
190
|
-
even with a single robot online.
|
|
191
|
-
|
|
192
|
-
### 3.2 OAuth "welcome back" animation
|
|
193
|
-
|
|
194
|
-
If the boot was triggered by an OAuth callback
|
|
195
|
-
(`consumeOAuthPending()` returns true), once we land on
|
|
196
|
-
`authenticated` with a known username, overlay `WelcomeBackView`
|
|
197
|
-
on top of the picker. A normal tab re-open (no redirect) does NOT
|
|
198
|
-
show the animation.
|
|
199
|
-
|
|
200
|
-
### 3.3 User clicks a robot → handing off
|
|
201
|
-
|
|
202
|
-
1. `selectRobot(robotId)` → phase `handing-off`.
|
|
203
|
-
2. `ReachyHost.tsx` mounts `<AppFrame>` with the iframe pointing
|
|
204
|
-
at `<same-origin>?embedded=1#creds=<base64>`.
|
|
205
|
-
3. The iframe boots through `index.html` again, taking the embed
|
|
206
|
-
branch.
|
|
207
|
-
4. `ConnectingView` is overlaid on top of the iframe (3-step
|
|
208
|
-
stepper), driven by the embed's `embed:app-state` messages.
|
|
209
|
-
|
|
210
|
-
### 3.4 ConnectingView (3 steps)
|
|
211
|
-
|
|
212
|
-
| Step | Internal name | Embed work |
|
|
213
|
-
|-----------|---------------|-----------------------------------------|
|
|
214
|
-
| Step 1 | `link` | `reachy.connect()` (open SSE) |
|
|
215
|
-
| Step 2 | `session` | `reachy.startSession(robotPeerId)` |
|
|
216
|
-
| Step 3 | `wake` | `reachy.ensureAwake()` |
|
|
217
|
-
|
|
218
|
-
When the embed reaches `phase: 'live'` the overlay fades out.
|
|
219
|
-
|
|
220
|
-
### 3.5 Top bar
|
|
221
|
-
|
|
222
|
-
Layout: `[icon] [app name] ........ [robot status] [end-session] [oauth menu]`
|
|
223
|
-
|
|
224
|
-
- Left: app icon + app name from `mountHost()`.
|
|
225
|
-
- Right: when `isLive`, an "End session" button; when also `isAwake`,
|
|
226
|
-
a motors-awake dot; always (when `userName` known), the OAuth
|
|
227
|
-
menu with @username and "Log out".
|
|
228
|
-
|
|
229
|
-
The top bar stays rendered through every phase of Mode A. It does
|
|
230
|
-
**not** render at all in Mode B.
|
|
231
|
-
|
|
232
|
-
### 3.6 End session
|
|
233
|
-
|
|
234
|
-
Triggered by:
|
|
235
|
-
- The user clicks "End session", OR
|
|
236
|
-
- The embed posts `embed:request-leave`, OR
|
|
237
|
-
- The window is about to unload (`pagehide`).
|
|
238
|
-
|
|
239
|
-
Flow:
|
|
240
|
-
1. Host → phase `stopping`, `AppFrame` stays mounted with
|
|
241
|
-
`LeavingView` overlay.
|
|
242
|
-
2. Host posts `host:leaving`.
|
|
243
|
-
3. Embed runs `onLeave` callbacks → `reachy.stopSession()` → posts
|
|
244
|
-
`embed:custom { channel: "leave-ack" }`.
|
|
245
|
-
4. Host receives ack (or hits `timeoutMs`) → `unselectRobot()` →
|
|
246
|
-
phase `picking`. Iframe unmounts.
|
|
247
|
-
|
|
248
|
-
---
|
|
249
|
-
|
|
250
|
-
## 4. Mode B: Mobile handoff flow
|
|
251
|
-
|
|
252
|
-
The mobile app opens the Space in a WebView with a pre-built URL
|
|
253
|
-
containing creds in the hash. The dispatcher loads `./embed`
|
|
254
|
-
directly. **No host shell is mounted.**
|
|
255
|
-
|
|
256
|
-
The user sees:
|
|
257
|
-
- **No** sign-in view (mobile already authenticated).
|
|
258
|
-
- **No** robot picker (mobile already picked).
|
|
259
|
-
- **No** welcome-back animation.
|
|
260
|
-
- **No** host top bar - if the app wants one, it draws it itself.
|
|
261
|
-
|
|
262
|
-
The embed reads creds from the hash synchronously, wipes the hash,
|
|
263
|
-
and runs the same `connect → startSession → ensureAwake` sequence.
|
|
264
|
-
|
|
265
|
-
There is **no end-session button** in Mode B. Closing the WebView
|
|
266
|
-
triggers `pagehide`, which fires `onLeave` callbacks and
|
|
267
|
-
`reachy.stopSession()` automatically.
|
|
268
|
-
|
|
269
|
-
### Bridge in Mode B
|
|
270
|
-
|
|
271
|
-
`window.parent === window`, so the postMessage bridge is a noop.
|
|
272
|
-
The embed never expects a `host:init` from a parent; creds come
|
|
273
|
-
from the hash. An optional `host:init` wait is best-effort with a
|
|
274
|
-
short timeout so the flow never blocks.
|
|
275
|
-
|
|
276
|
-
---
|
|
277
|
-
|
|
278
|
-
## 5. App-author contract
|
|
279
|
-
|
|
280
|
-
When porting a Reachy Mini app to this stack, the app author owns
|
|
281
|
-
exactly four files:
|
|
282
|
-
|
|
283
|
-
| File | Purpose |
|
|
284
|
-
|-----------------------------|--------------------------------------------------------|
|
|
285
|
-
| `index.html` | Theme bootstrap, SDK CDN tag, `<script src=dispatch>` |
|
|
286
|
-
| `src/dispatch.ts` | Two-branch boot (host shell vs embed) |
|
|
287
|
-
| `src/embed.{ts,tsx}` | The app itself; calls `connectToHost()` once |
|
|
288
|
-
| `Dockerfile` | HF Spaces packaging (nginx + entrypoint var injection) |
|
|
289
|
-
|
|
290
|
-
Nothing else is mandatory. React, Svelte, Vue, vanilla TS, plain
|
|
291
|
-
HTML - all fine. See §1 "Tech freedom" for the rationale.
|
|
292
|
-
|
|
293
|
-
### Reference apps (canonical clone targets)
|
|
294
|
-
|
|
295
|
-
Three apps in this monorepo are kept in sync with the current
|
|
296
|
-
spec and serve as canonical starting points:
|
|
297
|
-
|
|
298
|
-
- **`reachy_mini_emotions/`** - React 19 + MUI 7. Best starting
|
|
299
|
-
point for a UI-rich app. Demonstrates `robot.set_full_target()`
|
|
300
|
-
streaming, `?config=` deep-linking, and ThemeProvider wired to
|
|
301
|
-
`host:theme-changed`.
|
|
302
|
-
- **`reachy_mini_telepresence/`** - React 19 + MUI 7. Best
|
|
303
|
-
starting point if your app needs camera / media streams in
|
|
304
|
-
addition to motion control.
|
|
305
|
-
- **`reachy_mini_minimal_conversation/`** - Vanilla TS, no
|
|
306
|
-
framework. Best starting point if you want the smallest
|
|
307
|
-
possible runtime overhead, or if your app's main UI is just an
|
|
308
|
-
audio control surface.
|
|
309
|
-
|
|
310
|
-
Any divergence between these apps and this SPEC is a bug - either
|
|
311
|
-
in the apps or in the SPEC. Fix it in the same PR that ships the
|
|
312
|
-
divergence.
|
|
313
|
-
|
|
314
|
-
---
|
|
315
|
-
|
|
316
|
-
## 6. Protocol v1
|
|
317
|
-
|
|
318
|
-
Full type definitions in [`src/lib/protocol.ts`](./src/lib/protocol.ts).
|
|
319
|
-
Envelopes are JSON, carry `source: 'reachy-mini'` and `version: 1`.
|
|
320
|
-
Both sides validate `event.origin` against `window.location.origin`
|
|
321
|
-
before trusting the payload.
|
|
322
|
-
|
|
323
|
-
### 6.1 Messages
|
|
324
|
-
|
|
325
|
-
| Direction | Type | Purpose |
|
|
326
|
-
|-----------------|-------------------------------|-----------------------------------------------|
|
|
327
|
-
| embed → host | `embed:ready` | "I'm alive, send creds" |
|
|
328
|
-
| host → embed | `host:init` | Theme, signaling URL, hfToken, robotPeerId, config |
|
|
329
|
-
| embed → host | `embed:app-state` | Lifecycle phase + connecting sub-step |
|
|
330
|
-
| host → embed | `host:theme-changed` | Theme switched (no reload) |
|
|
331
|
-
| host → embed | `host:config-changed` | Config updated (no reload, mobile-driven) |
|
|
332
|
-
| host → embed | `host:leaving` | Tear-down request with `timeoutMs` |
|
|
333
|
-
| embed → host | `embed:request-leave` | App requests end-of-session |
|
|
334
|
-
| embed → host | `embed:error` | Error report (`{ message, fatal, detail? }`) |
|
|
335
|
-
|
|
336
|
-
Intentionally **not** in the v1 protocol:
|
|
337
|
-
|
|
338
|
-
- `embed:request-config-update` (apps don't push config upstream).
|
|
339
|
-
- `host:custom` / `embed:custom` (no free-form channel; if a real
|
|
340
|
-
need emerges, it lands as a typed message via the major bump).
|
|
341
|
-
- Any heartbeat / ping-pong messages (§11 backlog).
|
|
342
|
-
|
|
343
|
-
### 6.2 Versioning policy
|
|
344
|
-
|
|
345
|
-
- `version: 1` is the only version today.
|
|
346
|
-
- **Additive changes** (new optional field, new message type)
|
|
347
|
-
ship without a version bump.
|
|
348
|
-
- **Breaking changes** (removed field, changed semantics, removed
|
|
349
|
-
message type) bump to `version: 2`. The host MAY support both
|
|
350
|
-
versions for one release cycle, then drop v1.
|
|
351
|
-
- On unknown version, the receiver logs a warning and ignores the
|
|
352
|
-
message. No negotiation handshake.
|
|
353
|
-
|
|
354
|
-
### 6.3 Idempotency
|
|
355
|
-
|
|
356
|
-
- `host:leaving` may arrive twice; the embed runs `onLeave`
|
|
357
|
-
callbacks **once** (gated by a `pendingLeaveTokenRef`) and acks
|
|
358
|
-
every time.
|
|
359
|
-
- `host:init` may arrive twice (rare: bridge re-arm); the embed
|
|
360
|
-
treats the latest as authoritative and re-applies theme / config.
|
|
361
|
-
- The bridge is same-tab `postMessage` - order is guaranteed by
|
|
362
|
-
the browser; duplicates only happen on explicit re-sends.
|
|
363
|
-
|
|
364
|
-
---
|
|
365
|
-
|
|
366
|
-
## 7. State machines
|
|
367
|
-
|
|
368
|
-
### 7.1 Host phase (Mode A only)
|
|
369
|
-
|
|
370
|
-
```mermaid
|
|
371
|
-
stateDiagram-v2
|
|
372
|
-
[*] --> booting
|
|
373
|
-
booting --> sdk_missing: SDK load timeout
|
|
374
|
-
booting --> signed_out: authenticate() false
|
|
375
|
-
booting --> authenticated: authenticate() true
|
|
376
|
-
booting --> error: ctor / clientId error
|
|
377
|
-
|
|
378
|
-
signed_out --> booting: user clicks Sign in
|
|
379
|
-
authenticated --> connecting: auto
|
|
380
|
-
connecting --> connected: SSE welcome
|
|
381
|
-
connecting --> error: HTTP / network fail
|
|
382
|
-
connected --> picking
|
|
383
|
-
picking --> handing_off: selectRobot()
|
|
384
|
-
handing_off --> live: embed reports phase=live
|
|
385
|
-
handing_off --> error: embed:error { fatal: true }
|
|
386
|
-
|
|
387
|
-
live --> stopping: endSession() OR embed:request-leave
|
|
388
|
-
stopping --> picking: leave-ack OR timeout
|
|
389
|
-
|
|
390
|
-
error --> picking: retry() if SDK authed
|
|
391
|
-
error --> booting: retry() if no SDK
|
|
392
|
-
error --> signed_out: retry() if auth expired
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
### 7.2 Handoff sequence (host → embed)
|
|
396
|
-
|
|
397
|
-
Showcases §8.1 (single SDK per tab).
|
|
398
|
-
|
|
399
|
-
```mermaid
|
|
400
|
-
sequenceDiagram
|
|
401
|
-
participant U as User
|
|
402
|
-
participant H as Host
|
|
403
|
-
participant HS as Host SDK
|
|
404
|
-
participant C as Central
|
|
405
|
-
participant E as Embed (iframe)
|
|
406
|
-
participant ES as Embed SDK
|
|
407
|
-
|
|
408
|
-
U->>H: click robot card
|
|
409
|
-
H->>H: phase = handing-off, mount iframe
|
|
410
|
-
E->>E: parse #creds, replaceState wipe
|
|
411
|
-
E->>H: embed:ready
|
|
412
|
-
H->>HS: disconnect() [§8.1]
|
|
413
|
-
H->>E: host:init
|
|
414
|
-
E->>ES: connect() → startSession() → ensureAwake()
|
|
415
|
-
ES->>C: claim robot
|
|
416
|
-
C-->>ES: session live
|
|
417
|
-
E->>H: embed:app-state { phase: 'live' }
|
|
418
|
-
H->>H: hide ConnectingView overlay
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
---
|
|
422
|
-
|
|
423
|
-
## 8. Engineering invariants
|
|
424
|
-
|
|
425
|
-
Three hard invariants. A failure here is a regression - it must
|
|
426
|
-
produce a defined observable behaviour and must be covered by a
|
|
427
|
-
test (see §11).
|
|
428
|
-
|
|
429
|
-
### 8.1 Single live SDK per tab
|
|
430
|
-
|
|
431
|
-
**Contract**: at any instant, exactly one SDK instance per tab is
|
|
432
|
-
registered at the central.
|
|
433
|
-
|
|
434
|
-
**Mechanism**:
|
|
435
|
-
- Host mounts the SDK in `booting` and uses it for the picker.
|
|
436
|
-
- As soon as the embed posts `embed:ready`, the host calls
|
|
437
|
-
`disconnect()` on its SDK and zeroes its references.
|
|
438
|
-
- On `leave-ack` (or timeout), the host calls `connect()` again to
|
|
439
|
-
refresh the fleet and lands back on `picking`.
|
|
440
|
-
|
|
441
|
-
**Why**: removes the entire class of "Robot is busy" false
|
|
442
|
-
positives where the central thinks the shell still owns the robot.
|
|
443
|
-
Makes the central's debug output unambiguous.
|
|
444
|
-
|
|
445
|
-
**Defence in depth**: the host's SDK registers as
|
|
446
|
-
`<appName> (shell)` at the central; the embed keeps the clean
|
|
447
|
-
`appName`. This safety net protects the narrow window where both
|
|
448
|
-
SDKs overlap (between `embed:ready` and `disconnect()`).
|
|
449
|
-
|
|
450
|
-
### 8.2 Hash-only creds + immediate wipe
|
|
451
|
-
|
|
452
|
-
**Contract**: HF tokens never appear in URL search, never in
|
|
453
|
-
referer, never in HF Spaces access logs.
|
|
454
|
-
|
|
455
|
-
**Mechanism**:
|
|
456
|
-
- Creds are serialised as base64 JSON in the URL hash fragment
|
|
457
|
-
(`#creds=...`).
|
|
458
|
-
- The embed wipes the hash with `history.replaceState` on the
|
|
459
|
-
first synchronous tick of `connectToHost()`, before any `await`.
|
|
460
|
-
- On `host:leaving`, the embed clears `sessionStorage.hf_*` keys
|
|
461
|
-
before sending the `leave-ack`.
|
|
462
|
-
|
|
463
|
-
**Token TTL**: `hf_token_expires` is set to **15 min** at seed
|
|
464
|
-
time. The SDK refreshes on demand.
|
|
465
|
-
|
|
466
|
-
### 8.3 Bundle and SDK pinning
|
|
467
|
-
|
|
468
|
-
**Contract**: a fix in the host reaches every Space within one
|
|
469
|
-
cache cycle, with no per-app rebuild. A fix in the Reachy Mini SDK
|
|
470
|
-
can be rolled out by the SDK team, not the app teams.
|
|
471
|
-
|
|
472
|
-
**Pinning rules**:
|
|
473
|
-
- App bundles (`index-<hash>.js`): hashed by Vite, cache-busted
|
|
474
|
-
on deploy.
|
|
475
|
-
- Reachy Mini SDK in `index.html`: pinned to a **git tag or commit
|
|
476
|
-
SHA**, never to a branch. jsdelivr caches branches for ~12 h.
|
|
477
|
-
- `@pollen-robotics/reachy-mini-sdk/host` from CDN: pinned to a **major version**
|
|
478
|
-
(`@pollen-robotics/reachy-mini-sdk@1`) on jsdelivr/unpkg.
|
|
479
|
-
|
|
480
|
-
On detected mismatch (e.g. unknown protocol version, structurally
|
|
481
|
-
invalid `host:init`), the host's ErrorView's primary button calls
|
|
482
|
-
`window.location.reload(true)` to bypass any intermediary cache.
|
|
483
|
-
|
|
484
|
-
### 8.4 React Strict Mode safety
|
|
485
|
-
|
|
486
|
-
**Contract**: every effect in the host package survives a double
|
|
487
|
-
mount in `<React.StrictMode>` without doubling network I/O, SDK
|
|
488
|
-
instances, or postMessage traffic.
|
|
489
|
-
|
|
490
|
-
**Why this matters**: React 18+ in dev intentionally mounts every
|
|
491
|
-
component, runs every effect, runs every cleanup, then mounts and
|
|
492
|
-
runs effects again. Code that "looked fine" in prod will fire two
|
|
493
|
-
parallel `connect()` calls, leak two SSE subscribers, post
|
|
494
|
-
`embed:ready` twice, etc. In Mode A this surfaces as ghost
|
|
495
|
-
sessions at the central; in Mode B it surfaces as two competing
|
|
496
|
-
WebRTC peer connections.
|
|
497
|
-
|
|
498
|
-
**Mechanisms**:
|
|
499
|
-
|
|
500
|
-
- **Boot guard refs**: `useReachyHost()` uses a `bootStartedRef`
|
|
501
|
-
set on first mount; the second StrictMode invocation early-
|
|
502
|
-
returns instead of re-instantiating the SDK.
|
|
503
|
-
- **Subscription cleanup**: every `useEffect` returns its tear-
|
|
504
|
-
down. `robotsChanged`, `phaseChanged`, `welcome` are all
|
|
505
|
-
unsubscribed in the cleanup.
|
|
506
|
-
- **Idempotent SDK calls**: `connect()` is a no-op when the SDK
|
|
507
|
-
is already `connected` / `streaming`; the host relies on this
|
|
508
|
-
rather than gating with a flag.
|
|
509
|
-
- **`connectToHost()` is one-shot**: a module-level
|
|
510
|
-
`bootPromiseRef` returns the same promise for a second call,
|
|
511
|
-
rather than re-running the handshake.
|
|
512
|
-
|
|
513
|
-
**Checks**: the rebuild MUST verify this by mounting `<ReachyHost>`
|
|
514
|
-
inside `<StrictMode>` in dev and observing in DevTools:
|
|
515
|
-
|
|
516
|
-
1. Exactly one SDK instance ever exists at the central.
|
|
517
|
-
2. Exactly one `embed:ready` is posted per iframe mount.
|
|
518
|
-
3. No "subscription leak" warnings from React's effect runner.
|
|
519
|
-
|
|
520
|
-
If any of these fails, the implementation has a latent ordering
|
|
521
|
-
bug; fix before publishing.
|
|
522
|
-
|
|
523
|
-
---
|
|
524
|
-
|
|
525
|
-
## 9. Non-goals
|
|
526
|
-
|
|
527
|
-
To stay simple and auditable, this system explicitly does NOT do:
|
|
528
|
-
|
|
529
|
-
- **App discovery / listing / catalog**. The host shell renders
|
|
530
|
-
exactly **one** app: the one it ships with. Listing the available
|
|
531
|
-
Reachy Mini apps, searching them, categorising them - all of
|
|
532
|
-
that lives in the **Reachy Mini mobile app** and is fed by the
|
|
533
|
-
**website's `/api/js-apps` endpoint** (filtered on the
|
|
534
|
-
`reachy_mini_js_app` tag). The host has no notion of "other apps".
|
|
535
|
-
- **"Official app" badging in the host shell**. Officialness is
|
|
536
|
-
derived from the Space ID prefix (`pollen-robotics/*`, see §1)
|
|
537
|
-
and surfaces only in the mobile catalog. The host renders any
|
|
538
|
-
app the same way.
|
|
539
|
-
- **Multi-robot per tab**. One robot at a time per Space session.
|
|
540
|
-
- **Hot reload of the app code without reloading the iframe**.
|
|
541
|
-
Code updates require unmounting and remounting.
|
|
542
|
-
- **App ↔ app communication**. Apps are sandboxed by design.
|
|
543
|
-
- **Offline mode**. The central is required for every session.
|
|
544
|
-
- **Automatic WebRTC retry**. On a session drop, the user goes
|
|
545
|
-
back to the picker manually.
|
|
546
|
-
- **Queue or persistence of postMessage events** if the bridge is
|
|
547
|
-
down. The bridge is best-effort, both sides re-converge on the
|
|
548
|
-
next message.
|
|
549
|
-
- **Server-side rendering**. The host is a CSR shell, deliberately.
|
|
550
|
-
- **Cross-origin iframe**. The embed is same-origin with the host
|
|
551
|
-
(both served by the Space's nginx); we rely on this for the
|
|
552
|
-
origin check.
|
|
553
|
-
- **Vanilla / framework-agnostic host shell**. The host is
|
|
554
|
-
intentionally React + MUI (§1). Apps in any framework load this
|
|
555
|
-
shell only for the pre-session UI; once they go live in the
|
|
556
|
-
iframe, the shell's React tree is idle. Re-evaluating this is
|
|
557
|
-
in §11 backlog if real usage data ever justifies it.
|
|
558
|
-
- **Imposing a framework on app authors**. The reverse of the
|
|
559
|
-
point above: apps are completely free of framework constraints
|
|
560
|
-
inside the iframe (see §1 Tech freedom).
|
|
561
|
-
|
|
562
|
-
---
|
|
563
|
-
|
|
564
|
-
## 10. Threat model
|
|
565
|
-
|
|
566
|
-
The host runs in a HF Spaces container; the embed runs in a
|
|
567
|
-
same-origin iframe within the same Space. The threat surface is
|
|
568
|
-
narrow but not zero.
|
|
569
|
-
|
|
570
|
-
| Asset | Threat | Mitigation |
|
|
571
|
-
|--------------------------|------------------------------------------|------------------------------------------------|
|
|
572
|
-
| HF bearer token | Leak via URL log / referer | Hash-only + immediate wipe (§8.2), 15 min TTL |
|
|
573
|
-
| HF bearer token | Leak via sessionStorage to other origin | Same-origin embed, no cross-origin postMessage |
|
|
574
|
-
| `config` payload | Attacker controls URL → malformed JSON | App MUST validate at the boundary (typed cast is not enough) |
|
|
575
|
-
| postMessage channel | Random extension posts a forged message | Receivers check `source === 'reachy-mini'` AND `event.origin === window.location.origin` |
|
|
576
|
-
| Central session | Tab crashes, robot stays claimed | `pagehide` triggers `stopSession()`; central also enforces idle timeout |
|
|
577
|
-
|
|
578
|
-
**Out of scope** for this iteration:
|
|
579
|
-
- Defence against a malicious app that the user explicitly loaded.
|
|
580
|
-
We trust apps published under the HF Reachy Mini namespace.
|
|
581
|
-
- Defence against a compromised central. The signaling URL is
|
|
582
|
-
configurable per-Space; the central is the trust root.
|
|
583
|
-
|
|
584
|
-
---
|
|
585
|
-
|
|
586
|
-
## 11. Backlog
|
|
587
|
-
|
|
588
|
-
Items below are tracked but **not** in §8 yet. Promote to §8 only
|
|
589
|
-
when implemented and the cost of regressing them is justified.
|
|
590
|
-
|
|
591
|
-
- **CDN-only host**: the historical workaround of vendoring
|
|
592
|
-
`dist/` into `vendor/reachy-mini-host/` to placate HF Spaces'
|
|
593
|
-
Docker build (which refused `file:../` deps) is no longer
|
|
594
|
-
needed now that `@pollen-robotics/reachy-mini-sdk/host` is
|
|
595
|
-
published to npm. The dispatcher imports from the jsdelivr
|
|
596
|
-
bundle, so a host fix reaches every Space without per-app
|
|
597
|
-
redeploys.
|
|
598
|
-
- **Heartbeat / liveness**: detect a frozen embed (JS infinite
|
|
599
|
-
loop in a same-tab iframe). Trigger if we observe a freeze in
|
|
600
|
-
the wild. Until then, the user can hit "End session" manually.
|
|
601
|
-
- **Structured error codes**: today `embed:error` is `{ message,
|
|
602
|
-
fatal, detail? }`. If error reporting becomes a real ops need,
|
|
603
|
-
introduce a `code: 'busy' | 'auth' | 'network' | 'startup' |
|
|
604
|
-
'unknown'` enum.
|
|
605
|
-
- **Short-lived scoped tokens**: replace the raw HF token in
|
|
606
|
-
`CredsBundle.hfToken` with a session-scoped token issued by a
|
|
607
|
-
small backend.
|
|
608
|
-
- **Reconnect after session drop**: SDK could expose
|
|
609
|
-
`resumeSession()` so we re-run the WebRTC handshake without
|
|
610
|
-
going through the picker again.
|
|
611
|
-
- **Mobile parity for ConnectingView**: export a tiny
|
|
612
|
-
`<EmbedConnectingOverlay>` from `@pollen-robotics/reachy-mini-sdk/host/embed` so
|
|
613
|
-
Mode B apps reuse the 3-step visual without pulling React.
|
|
614
|
-
- **Observability hook**: opt-in callback for app authors / HF ops
|
|
615
|
-
to collect a redacted trace of every session.
|
|
616
|
-
- **Test suite**: unit tests on `useHostBridge` and
|
|
617
|
-
`connectToHost`, e2e Playwright run against a mock central. The
|
|
618
|
-
invariants in §8 are the prime testing target.
|