Skip to content

Remote access: a robot can belong to a Hugging Face account - #205

Open
pierre-rouanet wants to merge 5 commits into
policy-hub-designfrom
remote-access-design
Open

Remote access: a robot can belong to a Hugging Face account#205
pierre-rouanet wants to merge 5 commits into
policy-hub-designfrom
remote-access-design

Conversation

@pierre-rouanet

@pierre-rouanet pierre-rouanet commented Sep 2, 2026

Copy link
Copy Markdown
Member

Stacked on #191 (policy-hub-design), so the base is that branch rather than main — GitHub retargets it to main when 191 merges.

A duck on a LAN needs no account. Reaching one from outside does, and this is that
half: a robot can belong to a Hugging Face account, signed in from a terminal, a
phone over Bluetooth, or the console page. Nothing consumes the credential yet —
the relay is next, and this is what it needed to exist.

remote-webrtc.md §7 has said since it was written that the remote path is a bridge
to the signalling server already on the robot, and that whether we adopt the
rendezvous Space and how a robot is bound to an account were out of scope "until
local mode works". Local mode works.

What a person can do now

sudo robotctl account login      # prints a code; approve it anywhere; Ctrl-C is free
robotctl account status
sudo robotctl account logout

duckctl account login            # the same, over BLE — the only transport a robot
duckctl account status           # with no wifi has
$ sudo robotctl account login
Open https://hf.co/oauth/device and enter this code:

    A6MY-0314

Waiting for approval…
Signed in as PierreRouanet.

The flow is RFC 8628, and the shape is the requirement

login asks Hugging Face for a device code and answers with the code, not a token.
updaterd polls; a client comes back to status. That is not a convenience: a phone
that opens a browser to show somebody the code backgrounds itself, and iOS then tears
the GATT link down. A login that reported success by holding a connection open would
work from a laptop and fail from the device it is for.

Hugging Face ships a first-party public device-code client, which reachy_mini's
own device flow uses, so this registers no OAuth app anywhere.

Three invariants inherited from the mini's setup wizard, none of them about Python:
lead with the code and never auto-open a browser (auto-switching to Safari hid the
code before users could read it); a transport drop mid-flow is expected, not an error;
and appearing on the rendezvous — not a stored token — is the only real success signal.

Two things running the flow against the real service changed

A token lasts 30 days and its refresh token rotates. So the store is two strings
plus an absolute expiry, maintain renews at a week left rather than on the last day,
and the one window rotation leaves open — HF issued a new pair, the board lost power
before the write — is named in Store::save and surfaces in status, because no write
ordering can close it. A robot off for more than thirty days comes back needing a
login, which token_expires_in going negative is how a client says.

The token carries every scope HF grants: write-repos, manage-repos, jobs,
read-billing. That client takes no scope parameter. A duck holding a credential
that can push to its owner's repositories is worse than it needs to be for something
whose job is proving an identity — and a stolen board yields it. The fix is a public
device-code app in the org with openid profile read-repos: one constant in
account.rs, one click in HF. Not blocking — the flow works today and a scope
change is a re-login — but it should not ship without it. §2.4.

Where the credential lives

/etc/robot/hf-token, root:robot, 0640. updaterd owns it for policy.*'s
reasons — it is the daemon with a network stack, robotctl must not link one, and the
same credential is what reaches a private Hub repo. mediad will read it and must not
own it: unprivileged, ProtectHome=yes, and it is the process a remote peer talks to.

Not in robotd.toml, deliberately: configure --list, the config editor and the
"what changed on this robot" report would all print it.

Written 0600 and relaxed after the group is set, rather than through
fsutil::write_atomic, which sets no mode — a token that lands 0644 and is chmodded
a moment later is world-readable for that moment, which is invisible in testing and
permanent in whatever read it. A test asserts the landed file gives "others" nothing.

Routing it to WebRTC is the decision worth reading

account.login is the first mutating call that transport carries, and the only
one there whose effect outlives the session the way an account does: it converts having
been on the wifi once into remote access that outlives being there. remote-webrtc.md
§4 accepts that anyone on the network has the robot and its camera; it did not consider
anyone on the network having them from another continent next month.

Permitted anyway — the console is where somebody would sign a robot in, and the
alternative is ssh — with three properties that make it hold, argued in
mediad/src/route.rs:

  • a robot that already belongs to somebody refuses by name (--force to replace),
    so a LAN peer cannot silently take a robot from its owner;
  • it is visible: account.status names the account from any transport with no
    authorisation at all;
  • it is revocable from more places than the robot, including HF itself.

It also means mediad needed allow_users in updater.toml, so two files now have to
agree — hence only_these_mutating_calls_are_reachable_over_webrtc, the named list btd
has had since BLE could apply an update.

Writing that list down found two methods nobody had noticed were broken

policy.install and policy.fetch are routed to WebRTC on this branch, and mediad was
not in allow_users — so updaterd answered them PERMISSION_DENIED. The console
could offer a Hub browser whose install button could not work. The allow_users line
added here for the account fixes them too, and they are now in the named list with a note
saying how they were found.

That is the argument for a named list over a counted one: it is where a transport's
authority and a config file's grants are made to agree out loud.

The rendezvous, and what reading its source settled

Decided: pollen-robotics/reachy_mini_central, the Space the mini's fleet uses. We
maintain it
, so a duck-shaped need there is a pull request rather than a fork — and
the reverse is true too, which is worth knowing now that a second family of robots is
on it.

An earlier commit in this branch said its repository was private and reverse-read the
wire from the mini's client. That was a wrong-name 401 mistaken for a permissions
error. Reading app.py corrected five things:

  • the lease is 30 s and keyed only on inbound POST /send — a healthy SSE stream
    refreshes nothing;
  • the welcome advertises a 10 s cadence and no lease_seconds, so the middle rung
    of the mini relay's negotiation ladder is unreachable here;
  • the SSE side pings every 30 s of idle to survive the Space's proxy, which is what
    a read timeout has to be sized against;
  • the server gates concurrent sessions itself (sessionRejected, with the
    activeApp holding it), so a robot-side gate is belt-and-braces rather than a
    workaround — and it stays, because two remote writers into one intent slot is §9's
    interleaving bug;
  • meta is free-form to the protocol and not to the server: it reads hardware_id
    as a stable-identity key and evicts an older producer of the same user carrying the
    same value. So a duck must put its SoC serial there — producer.rs already reads it
    — or a robot that reconnects with a fresh token is listed twice, and putting the
    name there would fork a robot's identity on rename.

It also corrected remote-webrtc.md §7, which said the bridge "parses nothing" and is
"a relay rather than a translator". The payload stays opaque; the envelope does not —
the wire is HTTP (SSE in, POST out) with per-hop ids.

One hazard for whoever owns the image path

Peers are keyed by token. Two robots sharing one take turns being reachable, and
neither looks broken. Each duck runs its own device flow, so each has its own token —
unless an image is cloned with /etc/robot/hf-token in it, which is what this
project's flashing path does with everything else in /etc/robot. §3.7.

Wire

API_VERSION 21 → 22, additive — 21 went to robot.skills on the base branch while
this was in flight, which the rebase settled. account.login, account.status, account.logout on
updaterd; login and logout are is_mutating (so uid-authorised), status is a
read and stays ungated — which account a robot thinks it belongs to is the first thing
support asks.

Tested

Unit: the store's permissions and round trip, a corrupt credential reading as signed
out, the OAuth error classification (slow_down treated as failure would abandon a
login about to succeed), the device-code normalisation against exactly what
huggingface.co answered, and the refusals.

End to end over a real unix socket against a fake Hub that answers what HF answers:
the code arrives before anyone approves anything, a second client picks the flow
up (which is the property the whole shape exists for), the daemon polls through
authorization_pending, and what lands on disk is the pair rather than just the access
token. Plus both route tables' boundary tests.

The protocol itself was verified against the live Hugging Face endpoints.

Not run on a board. That is the gap.

What is next, and what is still open

The relay (§3): producer registration, the negotiated heartbeat, the split-brain poll
against /api/robot-status. Verifiable with no client at all — the Space's dashboard
counts a producer.

Open: the scope narrowing above (§2.4, pre-ship), and where the remote console page is
served from (§5) — which is the decision that actually couples us to the service.

@pierre-rouanet
pierre-rouanet changed the base branch from main to policy-hub-design September 2, 2026 15:29
@pierre-rouanet pierre-rouanet reopened this Sep 2, 2026
`remote-webrtc.md` §7 has said since it was written that the remote path is a
bridge to the signalling server already running on the robot, and that whether
we adopt reachy-mini's Hugging Face Space and how a robot is bound to an account
were out of scope "until local mode works". Local mode works.

So this designs the two things that shape needs and does not have: a credential
that names an account, and a service to present it to.

The account is an OAuth **device** flow, not the redirect flow reachy-mini runs.
That is not a preference — its flow points HF's callback at a URL on the robot,
which costs a registered redirect URI per hostname, requires the authorising
browser to resolve and reach the robot, and is why its mobile app carries a
loopback HTTP bridge whose stated purpose is avoiding an HF-side config change.
The device grant asks HF for a code, says "type M8HJ-FMGN at hf.co/oauth/device",
and polls. A phone on cellular can do it.

Established by probing the live services rather than by reading about them:
HF advertises the device grant and the full round trip short of the user's click
works today; the client must be public, so reachy-mini's confidential client id
cannot be reused; the rendezvous Space is live and 401s without a token; and its
wire is the gst envelopes over HTTP — SSE in, POST out — with per-hop ids, not
the WebSocket protocol a LAN client speaks.

That last one corrects §7 in this repository, which said the bridge "parses
nothing" and is "a relay rather than a translator". The payload stays opaque; the
envelope does not. Fixed there, with the two sides side by side in the new page.

Four decisions are open and named with who can close them: the OAuth client id
(one public app in the org, no secret — an admin's click), whether the token
expires (one real authorization, then read the response — a click), which
rendezvous, and where the remote client is served. The order of work puts the two
slices that need no client first, because both are verifiable on their own.

Assisted-by: Claude:claude-opus-5[1m]
Three calls — `account.login`, `account.status`, `account.logout` — reachable
locally, over BLE and over a WebRTC datachannel. Nothing consumes the credential
yet; that is the relay, and this is what it needs to exist.

The flow is RFC 8628. `login` asks Hugging Face for a device code and answers
with it; `updaterd` polls; a client comes back to `status`. That shape is the
requirement rather than a convenience: a phone that opens a browser to show
somebody the code backgrounds itself, and iOS then tears the GATT link down, so
a login that reported success by holding a connection open would work from a
laptop and fail from the device it is for.

Hugging Face ships a first-party public device-code client, so this registers no
OAuth app anywhere. Two things came out of running the flow against the real
service, and both changed the design:

A token lasts 30 days and its refresh token **rotates**. So the store is two
strings plus an absolute expiry, `maintain` renews at a week left rather than on
the last day, and the one window rotation leaves open — HF issued a new pair, the
board lost power before the write — is named in `Store::save` and surfaces in
`status`, because no write ordering can close it.

The token also carries **every scope HF grants**: write-repos, manage-repos,
jobs, read-billing. That client takes no `scope` parameter. A duck holding a
credential that can push to its owner's repositories is worse than it needs to
be for something whose job is proving an identity, and the fix is a public app in
the org with `openid profile read-repos` — one constant here, one click there.
Recorded as the thing to do before a duck ships rather than done, since the flow
works today and a scope change is a re-login.

`updaterd` owns it for `policy.*`'s reasons: the network stack, and a credential
that also reaches a private Hub repo. `/etc/robot/hf-token`, root:robot, 0640 —
not `robotd.toml`, where `configure --list`, the config editor and the "what
changed on this robot" report would all print it. Written 0600 and relaxed after
the group is set, rather than through `fsutil::write_atomic`, which does not set
a mode: a token that lands 0644 and is chmodded a moment later is world-readable
for that moment.

Routing `account.login` to WebRTC is the decision worth reading. It is the only
call on that transport whose effect outlives the session the way an account does
— it converts having been on the wifi once into remote access that outlives being
there. Permitted anyway, with the argument and its three mitigations in
`mediad::route`: a robot already signed in refuses by name, the binding is
readable by anybody without authorisation, and it is revocable from more places
than the robot.

That also means `mediad` needed `allow_users` in updater.toml, so two files now
have to agree — hence `only_these_mutating_calls_are_reachable_over_webrtc`, the
named list `btd` has had since BLE could apply an update.

**Writing that list down found two methods nobody had noticed were broken.**
`policy.install` and `policy.fetch` are routed to WebRTC and `mediad` was not in
`allow_users`, so `updaterd` answered them PERMISSION_DENIED: the console could
offer a Hub browser whose install button could not work. The line added here for
the account fixes them too. Which is the argument for a named list over a counted
one — it is where a transport's authority and a config file's grants are made to
agree out loud.

`API_VERSION` 21 → 22, additive.

Assisted-by: Claude:claude-opus-5[1m]
An earlier draft of this page said the Space's repository was private and
reverse-read its wire from the mini's client. That was a wrong-name 401 mistaken
for a permissions error: the server is `pollen-robotics/reachy_mini_central`,
we maintain it, and every number in §3 now comes off its `app.py`.

What reading it changed:

The lease is 30 s and keyed **only** on inbound `POST /send`, so a healthy SSE
stream refreshes nothing. The welcome advertises a 10 s cadence and publishes no
`lease_seconds`, which makes the middle rung of the mini relay's negotiation
ladder unreachable here — not worth reproducing for a field nothing sends. The
SSE side pings every 30 s of idle to keep the Space's proxy from dropping the
connection, which is what a read timeout has to be sized against.

The server gates concurrent sessions itself — `sessionRejected`, with the
`activeApp` that holds it, plus a `sessionStateChanged` push to the owner's other
devices. So a robot-side gate is belt-and-braces rather than a workaround, and it
stays: two remote writers into one intent slot is §9's interleaving bug.

`meta` is free-form to the protocol and not to the server, which is new: it reads
`hardware_id` as a stable-identity key and evicts an older producer of the same
user carrying the same value, ending its session. So a duck must put its SoC
serial there — `producer.rs` already reads it — or a robot that reconnects with a
fresh token is listed twice, and putting the *name* there would fork a robot's
identity on rename.

And one hazard that belongs to the flashing path: peers are keyed by **token**, so
two robots sharing one take turns being reachable, with nothing that looks like a
failure. Each duck runs its own device flow, so each has its own token — unless an
image is cloned with `/etc/robot/hf-token` in it, which is what this project's
image path does with everything else in `/etc/robot`.

The console's SSE transport is settled by the same read: the query-string token
form is deprecated server-side and going away, so the page is `fetch` plus SSE
line-splitting rather than `EventSource`, first time rather than twice.

Assisted-by: Claude:claude-opus-5[1m]
`account login` printed a code and a URL and left you to copy them. The reason
was an invariant inherited from the mini's setup wizard — never open a browser by
yourself — and applying it here was wrong: that rule is about a *phone*, where the
browser replaces the only screen and backgrounds the app, so a code shown a moment
earlier is gone before it is read. A terminal keeps it in the scrollback.

So the rule is a property of the surface rather than a rule, and it gives three
answers. `robotctl` opens nothing, because it runs on the robot and the robot has
no display. `duckctl` opens, because it runs on your machine — where `duckctl
open` already launches a browser, so this needs no new dependency and follows a
pattern that exists. A phone app keeps the mini's rule for the mini's reason.

It opens `verification_uri_complete`, which is worth the difference: Hugging Face
sends no such field, so the robot synthesises the `?user_code=` form, and HF
preserves that parameter across its own login redirect — so the code is filled in
even in a browser that was not signed in yet. Nothing to type at all.

Three things the ordering has to get right, and each is a line in the note:

The code is printed **before** the browser opens, so a browser that will not
launch leaves the instructions on screen instead of an error where they should
have been. A failed open is a warning appended to the code, never a failure of the
command — unlike `duckctl open`, where opening the browser *is* the command.

`--no-open` skips it, and so does stderr not being a terminal: a script that opens
a browser window on whoever runs it is a surprise rather than a convenience.

Assisted-by: Claude:claude-opus-5[1m]
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Coverage

71.04% lines on this branch, against a floor of 72%.

Per-file
Filename                              Regions    Missed Regions     Cover   Functions  Missed Functions  Executed       Lines      Missed Lines     Cover    Branches   Missed Branches     Cover
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
btd/src/adv.rs                             79                 0   100.00%           9                 0   100.00%          42                 0   100.00%           0                 0         -
btd/src/bluez.rs                          422               422     0.00%          32                32     0.00%         279               279     0.00%           0                 0         -
btd/src/chorale.rs                        346               246    28.90%          23                13    43.48%         230               171    25.65%           0                 0         -
btd/src/framing.rs                        217                 7    96.77%          17                 1    94.12%          97                 5    94.85%           0                 0         -
btd/src/gatt.rs                             3                 0   100.00%           1                 0   100.00%           3                 0   100.00%           0                 0         -
btd/src/link.rs                            13                 0   100.00%           1                 0   100.00%          17                 0   100.00%           0                 0         -
btd/src/main.rs                            77                77     0.00%          11                11     0.00%          77                77     0.00%           0                 0         -
btd/src/pairing.rs                        149                 2    98.66%          12                 1    91.67%          81                 1    98.77%           0                 0         -
btd/src/route.rs                          345                32    90.72%          27                 0   100.00%         344                11    96.80%           0                 0         -
btd/src/session.rs                       1118                71    93.65%          66                 5    92.42%         649                41    93.68%           0                 0         -
btd/src/upstream.rs                       297                43    85.52%          29                 8    72.41%         184                25    86.41%           0                 0         -
configd/src/bluez.rs                      441               441     0.00%          78                78     0.00%         297               297     0.00%           0                 0         -
configd/src/identity.rs                   111                 5    95.50%          12                 1    91.67%          52                 3    94.23%           0                 0         -
configd/src/main.rs                       494               494     0.00%          38                38     0.00%         338               338     0.00%           0                 0         -
configd/src/net.rs                        246                12    95.12%          38                 2    94.74%         181                 4    97.79%           0                 0         -
configd/src/nm.rs                         271               213    21.40%          43                39     9.30%         171               120    29.82%           0                 0         -
configd/src/pad.rs                        301                12    96.01%          33                 2    93.94%         186                 8    95.70%           0                 0         -
configd/src/power.rs                       30                30     0.00%           6                 6     0.00%          28                28     0.00%           0                 0         -
configd/src/store.rs                      350                25    92.86%          28                 2    92.86%         173                 9    94.80%           0                 0         -
configd/src/units.rs                      119               119     0.00%          18                18     0.00%          83                83     0.00%           0                 0         -
duck-control/src/bus.rs                   414               305    26.33%          32                20    37.50%         278               205    26.26%           0                 0         -
duck-control/src/fall.rs                  173                 0   100.00%          15                 0   100.00%         108                 0   100.00%           0                 0         -
duck-control/src/imu.rs                   370                 8    97.84%          20                 0   100.00%         195                 5    97.44%           0                 0         -
duck-control/src/io.rs                    127                19    85.04%          20                 5    75.00%         115                17    85.22%           0                 0         -
duck-control/src/model.rs                 113                 1    99.12%          14                 0   100.00%          68                 0   100.00%           0                 0         -
duck-control/src/obs.rs                   260                 3    98.85%          23                 1    95.65%         174                 5    97.13%           0                 0         -
duck-control/src/policy.rs                370               229    38.11%          45                25    44.44%         253               148    41.50%           0                 0         -
duck-control/src/safety.rs                410                12    97.07%          31                 2    93.55%         274                 6    97.81%           0                 0         -
duck-detect/src/bin/duck-bench.rs         306               306     0.00%          16                16     0.00%         170               170     0.00%           0                 0         -
duck-detect/src/lib.rs                    512                10    98.05%          24                 0   100.00%         274                 6    97.81%           0                 0         -
duck-detect/src/onnx.rs                    96                96     0.00%           5                 5     0.00%          52                52     0.00%           0                 0         -
duck-detect/src/rknn.rs                   267               237    11.24%          15                11    26.67%         216               195     9.72%           0                 0         -
duck-ipc-proto/src/lib.rs                1995               154    92.28%         127                13    89.76%        1457                88    93.96%           0                 0         -
duckctl/src/main.rs                      2281               930    59.23%         136                43    68.38%        1447               599    58.60%           0                 0         -
kinematics/src/hand.rs                    373                 2    99.46%          21                 2    90.48%         172                 2    98.84%           0                 0         -
kinematics/src/head.rs                    381                 9    97.64%          20                 0   100.00%         188                 4    97.87%           0                 0         -
kinematics/src/lib.rs                     229                11    95.20%          19                 2    89.47%         115                 7    93.91%           0                 0         -
kinematics/src/math.rs                    171                 0   100.00%          16                 0   100.00%          79                 0   100.00%           0                 0         -
kinematics/src/mjcf.rs                    225                32    85.78%          17                 1    94.12%         132                15    88.64%           0                 0         -
kinematics/src/tof.rs                     338                20    94.08%          14                 1    92.86%         184                13    92.93%           0                 0         -
mediad/src/config.rs                       90                 5    94.44%           7                 1    85.71%          46                 4    91.30%           0                 0         -
mediad/src/detect.rs                      293               222    24.23%          13                 9    30.77%         200               145    27.50%           0                 0         -
mediad/src/exposure.rs                    412               161    60.92%          28                 8    71.43%         279               110    60.57%           0                 0         -
mediad/src/main.rs                        245               245     0.00%           7                 7     0.00%         158               158     0.00%           0                 0         -
mediad/src/pipeline.rs                    977               909     6.96%          62                56     9.68%         624               578     7.37%           0                 0         -
mediad/src/producer.rs                    161                33    79.50%          20                 5    75.00%         109                20    81.65%           0                 0         -
mediad/src/route.rs                       173                19    89.02%          12                 0   100.00%         148                 8    94.59%           0                 0         -
mediad/src/session.rs                     493                12    97.57%          30                 0   100.00%         290                 8    97.24%           0                 0         -
mediad/src/upstream.rs                    123                23    81.30%          10                 1    90.00%          82                17    79.27%           0                 0         -
mediad/src/web.rs                         112                26    76.79%          14                 4    71.43%          76                17    77.63%           0                 0         -
odometry/src/lib.rs                       321                 5    98.44%          22                 1    95.45%         192                 7    96.35%           0                 0         -
padd/src/main.rs                          498               498     0.00%          11                11     0.00%         338               338     0.00%           0                 0         -
padd/src/tap.rs                           693               455    34.34%          42                25    40.48%         419               267    36.28%           0                 0         -
pet-detect/src/bin/detect.rs               61                61     0.00%           1                 1     0.00%          38                38     0.00%           0                 0         -
pet-detect/src/bin/features.rs             46                46     0.00%           2                 2     0.00%          20                20     0.00%           0                 0         -
pet-detect/src/lib.rs                     358               180    49.72%          27                13    51.85%         217               115    47.00%           0                 0         -
pet-detect/src/worker.rs                  293               293     0.00%          17                17     0.00%         223               223     0.00%           0                 0         -
robotctl/src/configure.rs                 846               329    61.11%          35                12    65.71%         464               226    51.29%           0                 0         -
robotctl/src/duck.rs                     1211               144    88.11%          63                 1    98.41%         604                47    92.22%           0                 0         -
robotctl/src/main.rs                     5351              2769    48.25%         303               142    53.14%        3586              1857    48.22%           0                 0         -
robotctl/src/monitor.rs                  4024               724    82.01%         213                35    83.57%        2522               464    81.60%           0                 0         -
robotctl/src/path_map.rs                  425                59    86.12%          24                 2    91.67%         223                36    83.86%           0                 0         -
robotctl/src/show.rs                      714                46    93.56%          31                 1    96.77%         509                25    95.09%           0                 0         -
robotd-params/src/edit.rs                1323               114    91.38%          88                12    86.36%         630                50    92.06%           0                 0         -
robotd-params/src/lib.rs                 1835                91    95.04%         173                10    94.22%        1320                58    95.61%           0                 0         -
robotd-params/src/registry.rs             184                10    94.57%          13                 2    84.62%         138                17    87.68%           0                 0         -
robotd/src/chorale.rs                    1366                30    97.80%          71                 4    94.37%         826                20    97.58%           0                 0         -
robotd/src/control.rs                     405               366     9.63%          28                23    17.86%         322               274    14.91%           0                 0         -
robotd/src/intents.rs                     400                68    83.00%          45                 8    82.22%         294                45    84.69%           0                 0         -
robotd/src/main.rs                       6660              1742    73.84%         315                63    80.00%        4331              1132    73.86%           0                 0         -
robotd/src/soc.rs                          47                25    46.81%           4                 1    75.00%          30                15    50.00%           0                 0         -
robotd/src/sound.rs                       968               688    28.93%          50                33    34.00%         565               416    26.37%           0                 0         -
robotd/src/theremin.rs                    472                71    84.96%          32                 6    81.25%         271                38    85.98%           0                 0         -
sounds/src/chorale/beat.rs                403                16    96.03%          26                 1    96.15%         246                12    95.12%           0                 0         -
sounds/src/chorale/midi.rs               1047                80    92.36%          44                 2    95.45%         581                43    92.60%           0                 0         -
sounds/src/chorale/mod.rs                1423                57    95.99%          83                 3    96.39%         812                41    94.95%           0                 0         -
sounds/src/chorale/text.rs                700                60    91.43%          41                12    70.73%         376                17    95.48%           0                 0         -
sounds/src/lib.rs                         162                30    81.48%          14                 6    57.14%          84                18    78.57%           0                 0         -
sounds/src/main.rs                        463               463     0.00%          20                20     0.00%         240               240     0.00%           0                 0         -
sounds/src/personality.rs                 174                 0   100.00%           6                 0   100.00%          84                 0   100.00%           0                 0         -
sounds/src/rng.rs                         173                 0   100.00%          18                 0   100.00%          96                 0   100.00%           0                 0         -
sounds/src/stream.rs                      719                 7    99.03%          46                 1    97.83%         420                 6    98.57%           0                 0         -
sounds/src/synth.rs                       353                 7    98.02%          29                 0   100.00%         197                 3    98.48%           0                 0         -
sounds/src/voices.rs                      679                 2    99.71%          26                 0   100.00%         353                 2    99.43%           0                 0         -
test-support/src/lib.rs                   272                 2    99.26%          22                 0   100.00%         167                 0   100.00%           0                 0         -
tof/src/lib.rs                             88                 0   100.00%           9                 0   100.00%          54                 0   100.00%           0                 0         -
tof/src/main.rs                           478               438     8.37%          23                18    21.74%         303               281     7.26%           0                 0         -
tof/src/sensor.rs                         230               148    35.65%          22                15    31.82%         163               114    30.06%           0                 0         -
tof/src/status.rs                          76                 2    97.37%           7                 1    85.71%          58                 1    98.28%           0                 0         -
updater/src/account.rs                    918               228    75.16%          94                33    64.89%         599               149    75.13%           0                 0         -
updater/src/config.rs                     436                24    94.50%          38                 4    89.47%         354                17    95.20%           0                 0         -
updater/src/engine.rs                    3234               468    85.53%         215                27    87.44%        2090               286    86.32%           0                 0         -
updater/src/faults.rs                      69                 7    89.86%           7                 0   100.00%          51                 0   100.00%           0                 0         -
updater/src/fsutil.rs                     104                30    71.15%          10                 6    40.00%          51                24    52.94%           0                 0         -
updater/src/hooks.rs                      454                15    96.70%          37                 2    94.59%         382                12    96.86%           0                 0         -
updater/src/ipc.rs                        901               308    65.82%          67                20    70.15%         559               167    70.13%           0                 0         -
updater/src/journal.rs                    832                78    90.62%          57                10    82.46%         483                53    89.03%           0                 0         -
updater/src/lib.rs                         44                13    70.45%           4                 0   100.00%          34                13    61.76%           0                 0         -
updater/src/main.rs                       572               244    57.34%          38                14    63.16%         426               157    63.15%           0                 0         -
updater/src/manifest.rs                   159                 6    96.23%          16                 0   100.00%         110                 1    99.09%           0                 0         -
updater/src/orphan.rs                     288                 6    97.92%          25                 0   100.00%         181                 3    98.34%           0                 0         -
updater/src/policy.rs                    1219               525    56.93%          97                46    52.58%         647               295    54.40%           0                 0         -
updater/src/preflight.rs                  382                16    95.81%          47                 4    91.49%         284                11    96.13%           0                 0         -
updater/src/reconcile.rs                  292                15    94.86%          25                 3    88.00%         172                 5    97.09%           0                 0         -
updater/src/robot.rs                      219                27    87.67%          34                 7    79.41%         133                15    88.72%           0                 0         -
updater/src/source/github.rs              474               169    64.35%          51                26    49.02%         304                99    67.43%           0                 0         -
updater/src/source/hf_hub.rs              136                72    47.06%          20                12    40.00%          81                41    49.38%           0                 0         -
updater/src/source/http.rs                319                82    74.29%          29                11    62.07%         260                88    66.15%           0                 0         -
updater/src/source/local.rs               346                37    89.31%          36                 9    75.00%         192                26    86.46%           0                 0         -
updater/src/source/mod.rs                  39                30    23.08%           4                 2    50.00%          30                24    20.00%           0                 0         -
updater/src/spawn.rs                       94                13    86.17%           5                 0   100.00%          49                 4    91.84%           0                 0         -
updater/src/store.rs                      593                41    93.09%          43                 7    83.72%         271                42    84.50%           0                 0         -
updater/src/transcript.rs                 518                42    91.89%          31                 3    90.32%         290                22    92.41%           0                 0         -
updater/src/verify.rs                     786                95    87.91%          55                18    67.27%         424                75    82.31%           0                 0         -
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL                                   67288             19007    71.75%        4486              1263    71.85%       42248             12237    71.04%           0                 0         -

…han a test

`duckctl account login` opened a URL with `?user_code=` appended, and both the
design page and the field's own documentation said the code would arrive filled
in. It does not. Hugging Face's device page ignores the parameter — it survives
the login redirect and prefills nothing — which a browser showed within minutes
of it shipping.

The claim traces back to a line in `reachy_mini`'s setup notes saying
`huggingface_hub` synthesises the `?user_code=` form. It does not: its
`request_device_code` falls back to `verification_uri` unchanged, which is
readable in four lines of the file this was otherwise copied from. What I checked
instead was that the query parameter survived HF's login redirect, and that
proves only that a redirect preserves a query string.

So the fallback is now the plain URI, matching the hub. The field stays in the
reply because it is RFC 8628's, so a server that starts sending a real one is used
without a wire change — and the `urlencode` helper that existed only to build the
invented URL goes with it.

What `duckctl` opening the page buys is the navigation, not the typing. That is
still worth having, and the ordering that matters is unchanged: the code is
printed before the browser opens.

Assisted-by: Claude:claude-opus-5[1m]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant