Give identity its due: native enrollment, honest deletion, and sign-in that stays home
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -898,3 +898,218 @@ run on their own, and both pass:
|
||||
system ones (`disks-contract`, `snapshots-contract`, `containers-contract`),
|
||||
then the harness ones (`settings-search-contract`, `health-ui-contract`), and
|
||||
`settings-pages-contract` last, as before.
|
||||
|
||||
## Phase 11 (Users & Accounts) — append below
|
||||
|
||||
Spec: `2026-08-24-users-accounts-redesign.md`. Users grew a profile hero with a
|
||||
stock-avatar gallery and a Remove, a password flow with a strength meter, a
|
||||
live-validating add-user form, a keep-files deletion choice, and per-user
|
||||
management (type / reset password / unlock). Fingerprint enrollment stopped
|
||||
handing people to GNOME's Users panel and became native — fprintd's
|
||||
`EnrollStart` driven from `panama-fingerprint`, streaming one JSON line per
|
||||
touch — and the card learned to appear in the state that needs acting on rather
|
||||
than only when a reader is attached. Online Accounts gained native Nextcloud and
|
||||
IMAP forms, confirmed removal, and an availability that no longer collapses on a
|
||||
failed write.
|
||||
|
||||
Three agents edited the tree concurrently. Everything below was reconciled
|
||||
against the landed files at the end of the phase, not against the spec's pinned
|
||||
shapes: three of the needles moved during the phase (the card-visibility
|
||||
expression went into the service as `cardVisible`, the add-user field became
|
||||
`LiveFieldRow`, and the fingerprint helper became Python) and each is pinned
|
||||
where it actually landed.
|
||||
|
||||
### New contracts (1)
|
||||
|
||||
`quickshell/online-accounts-contract`. The README count line moves
|
||||
**171 → 172**; `setup/readme-contract` was run and passes ("172 contracts, as
|
||||
documented").
|
||||
|
||||
### Run and passing
|
||||
|
||||
- **`quickshell/fingerprint-contract` — RUN END TO END, PASS.** The whole file
|
||||
was reworked, because two of its needles inverted. Hermetic: `env -i`, a stub
|
||||
directory first on `PATH` (asserted, before the helper runs, that
|
||||
fprintd-list, authselect, sudo and pkexec all resolve inside it), both bus
|
||||
addresses pointed at sockets that do not exist, `PANAMA_PATH` pointed at a
|
||||
recording `panama-sudo` that runs **nothing**, and enrollment replayed from
|
||||
`PANAMA_FINGERPRINT_FIXTURE` with the calls it stood in for landing in
|
||||
`PANAMA_FINGERPRINT_LOG`. What it pins:
|
||||
- **The inverted needle.** `openGnomePanel("system", "users")` is now a
|
||||
failure rather than a requirement, checked across every settings page.
|
||||
`gnome-handoff-contract` lost its `UsersPage.qml:system-users` exception at
|
||||
the same time; this is the other half of that, said from the page's side.
|
||||
- **The stuck state, from the data.** With the stub fprintd reporting no
|
||||
devices and the stub authselect reporting `with-fingerprint`, `status` must
|
||||
still answer `unlockFeatureEnabled: true` — the combination that used to be
|
||||
unrepresentable, because the feature was only read when fprintd answered.
|
||||
This machine is in that state, which is how it was found.
|
||||
- **Card visibility followed one level of indirection.** The expression moved
|
||||
into `services/Fingerprint.qml` as `cardVisible: readerPresent ||
|
||||
unlockFeatureEnabled`; the contract accepts either the page spelling it out
|
||||
or the page binding to a service property that does, and resolves the second
|
||||
rather than forbidding it.
|
||||
- **The enrollment stream as a stream.** A five-stage fixture with a retry in
|
||||
the middle produces nine lines; every one must be a JSON object on its own,
|
||||
the counter monotonic and inside `0..total`, `total` constant, and the
|
||||
retried touch must **not** have advanced the counter — a panel that counts a
|
||||
failed touch promises a finish that never arrives. The terminal line says
|
||||
which way it ended.
|
||||
- **Claim and Release bracket everything**, read from the call log: first call
|
||||
`Claim`, last call `Release`, `EnrollStart` after the claim — including on a
|
||||
failed enrollment, because a reader left claimed refuses the next attempt
|
||||
with "busy with something else" forever.
|
||||
- **A terminal failure is reported in words**, not in fprintd's vocabulary
|
||||
(`enroll-duplicate` → "That finger is already enrolled").
|
||||
- **Nonsense is refused before the reader is touched**: four bad finger names
|
||||
against both `enroll` and `remove`, each of which must leave the call log
|
||||
empty.
|
||||
- **`remove` deletes the finger it was asked about**, and `remove-all` uses
|
||||
`DeleteEnrolledFingers2` with no arguments — its predecessor took a user
|
||||
name, which is how a typo deletes somebody else's prints.
|
||||
- **With no fixture and no bus it still answers in its own shape**: the page
|
||||
reads this stream line by line, and a traceback leaves an enrollment panel
|
||||
open forever.
|
||||
- **The stated reason survived the rewrite to Python**: the escalation must go
|
||||
through `panama-sudo --reason …`, and a bare `sudo`/`pkexec` in the argv log
|
||||
is a failure.
|
||||
- **Finger vocabulary**: a single id elsewhere is a default choice and is
|
||||
allowed; two in one file is a copy of the list, and the copy is what falls
|
||||
behind.
|
||||
- **`quickshell/user-accounts-contract` — RUN END TO END, PASS.** Its snapshot
|
||||
half still reads the live account service, which is read-only; every verb that
|
||||
would change something is pinned from the source and never invoked
|
||||
(`set-icon gib ""` really does clear the avatar). New halves:
|
||||
- **`reset-password` carries no password material**, by AST: it must call
|
||||
`SetPasswordMode`, must resolve its mode argument to **1** (through the
|
||||
named constant, so the meaning survives), must not call `SetPassword`, and
|
||||
its body — docstring excluded, because the docstring is allowed to say the
|
||||
word — must not mention stdin, openssl, crypt or passwd.
|
||||
- **keep-files end to end, as three separate links**, because any one of them
|
||||
can invert on its own: the helper derives `DeleteUser`'s boolean from its
|
||||
own argument (a literal there is the bug), the service's ternary is checked
|
||||
against the *polarity of its own parameter name* (Panama flipped this once
|
||||
already, `removeFiles` → `keepFiles`), and the page passes a variable rather
|
||||
than a constant. Both vocabularies are exercised — `keep`/`remove` and the
|
||||
legacy `keep-files`/`remove-files` — by reading which refusal comes back,
|
||||
so a vocabulary that stopped being understood shows up as the wrong message
|
||||
rather than as no message.
|
||||
- **`removeIcon` sends an empty path**, and `set_icon` recognizes it rather
|
||||
than handing "" to GdkPixbuf, which would fail and leave the avatar.
|
||||
- **The live-validation regression, two ways.** Structurally: the property the
|
||||
form's user-name test reads must be assigned from a per-keystroke handler,
|
||||
and the component that assigns it must not be `TextFieldRow`. Bluntly: the
|
||||
page carries no `TextFieldRow` at all. The page's regex is compared
|
||||
character for character against the helper's `USERNAME`, and the 31-cap must
|
||||
appear in the page's own text — a form that accepts a name accountsservice
|
||||
then refuses has no way to say why.
|
||||
- **The last administrator is refused helper-side too**, not only by the page
|
||||
that offers the control.
|
||||
- **Stock avatars**: shape read from a real run (`{name, path}`, absolute
|
||||
paths, empty list legitimate).
|
||||
- **`quickshell/gnome-handoff-contract` — RUN, PASS** (9 handoffs, 39 pages).
|
||||
The `UsersPage.qml:system-users` exception is deleted; the
|
||||
`OnlineAccountsPage.qml:online-accounts` one stays with its reason rewritten
|
||||
to name OAuth specifically — libgoa-backend ships without a GIR binding, so
|
||||
the provider's own dialog is the only way to obtain a token, and Nextcloud and
|
||||
IMAP are added on the page itself.
|
||||
- **`setup/readme-contract` — RUN, PASS** after the count line moved.
|
||||
|
||||
### Run with one half relaxed (1)
|
||||
|
||||
- **`quickshell/online-accounts-contract` — everything passes except six
|
||||
input-validation cases**, which are pinned as the spec asks and which the
|
||||
landed `panama-accounts` does not yet satisfy. See "Still open" below. Run
|
||||
with those six removed, the contract passes end to end. It is hermetic: `env
|
||||
-i`, a stub directory first on `PATH` (gdbus/busctl/dbus-send/
|
||||
gnome-control-center all resolve there, asserted before running), a `gi`
|
||||
stand-in on `PYTHONPATH` whose `require_version` always raises — so if the
|
||||
fixture seam were ever removed the contract would stop working rather than
|
||||
quietly start editing the session's accounts — and both bus addresses pointed
|
||||
at sockets that do not exist. Accounts come from `PANAMA_ACCOUNTS_FIXTURE`;
|
||||
what each verb would have done lands in `PANAMA_ACCOUNTS_LOG`. What it pins:
|
||||
- **Availability is not an error**, at the definition and at every assignment:
|
||||
no line that decides `available` may mention a write error, and the service
|
||||
must keep at least two error strings, or "GOA did not answer" and "that one
|
||||
change did not happen" are the same fact. This is the regression where one
|
||||
refused toggle replaced four working accounts with "not available".
|
||||
- **The same thing from the data**: a refused add must come back with the
|
||||
error *and* the full account list, because the page draws its list from the
|
||||
same answer that carries the message.
|
||||
- **The password on stdin, proved rather than assumed.** The log records the
|
||||
password's **length**, never the thing itself, so the sentinel can be
|
||||
checked for everywhere it might have gone (stdout, stderr, the log, the
|
||||
scratch home) while `passwordBytes` proves it was read at all rather than
|
||||
dropped.
|
||||
- **The listing cannot carry a credential**, read from `describe`'s own dict
|
||||
keys and from the GOA properties it reads, so a password property added
|
||||
later is caught at the source rather than after it reaches the page.
|
||||
- **An added account is proved, not assumed**: `call_add_account_sync` must be
|
||||
followed by `call_ensure_credentials_sync`, and `verify` must be able to
|
||||
`call_remove_sync` — GOA does not check what it is handed, so without that
|
||||
step a mistyped password produces an account that exists, looks correct, and
|
||||
never syncs, which is the exact failure this page exists to explain.
|
||||
- **Provider types**: `owncloud` (the fork kept the id) and `imap_smtp`. A
|
||||
provider type GOA does not know is not an error — AddAccount is simply never
|
||||
offered it.
|
||||
- **Both names for the listing** (`list` and `snapshot`) answer the same
|
||||
thing, and reading the list changes nothing.
|
||||
- **Removal carries the path intact** — removing the wrong account is
|
||||
unrecoverable and looks like a success — and is confirmation-gated on the
|
||||
page, read from the object block around the call rather than from a
|
||||
`confirming` property declared anywhere in the file.
|
||||
|
||||
### Docs updated in the same wave
|
||||
|
||||
- `services/SettingsSearch.qml` — nine entries added, all routing to leaves that
|
||||
exist in `SettingsRoutes`: **Fingerprint**, **Enroll a fingerprint**, **Delete
|
||||
a user**, **Account type**, **Reset a password** → `users`; **Nextcloud**,
|
||||
**Google account**, **Add a mail account**, **Remove an account** →
|
||||
`accounts`. Checked by evaluating the array: 163 entries, no duplicate labels,
|
||||
every `page` a real leaf, and none of `settings-search-contract`'s 21 ranked
|
||||
queries or 9 leaf-routing queries changes its top result — the only new entry
|
||||
any of them touches at all is Fingerprint, which matches `lock` through
|
||||
"Unlock" and sits far below the existing first hit.
|
||||
- No settings docs regenerated: this phase adds no schema keys. Every new
|
||||
setting is system state (accountsservice, fprintd, authselect, GOA), not a
|
||||
Panama preference — verified by reading the diff.
|
||||
|
||||
### Still open before the run
|
||||
|
||||
- **`online-accounts-contract` fails six validation cases against the landed
|
||||
helper**, and they are left pinned rather than relaxed, because the spec asks
|
||||
for them and each produces an account that exists, looks right in the list,
|
||||
and never syncs. `nextcloud_uris` accepts anything with a host once `https://`
|
||||
is prepended, and `imap_account` accepts any non-empty string as a server:
|
||||
- `add-nextcloud` accepts `not a url`, `https://cloud.example.org; reboot`,
|
||||
and `https://cloud example org`;
|
||||
- `add-imap` accepts `imap example com`, `imap.example.com; reboot`, and
|
||||
`-imap.example.com` as either server.
|
||||
|
||||
A hostname check in `nextcloud_uris` and one applied to both hosts in
|
||||
`imap_account` closes all six. `''`, `ftp://…` and `https://` (no host) are
|
||||
already refused, and every other assertion in the file passes.
|
||||
- **`settings-search-contract` has not been run**: it starts a Quickshell
|
||||
harness. The nine new entries were checked statically as described above.
|
||||
- **`settings-pages-contract`, `settings-docs-contract`, `settings-jump-contract`
|
||||
and `settings-buttons-contract` were not run against the two rebuilt pages.**
|
||||
Six new components landed (LiveFieldRow, SecretFieldRow, PasswordStrengthRow,
|
||||
StockAvatarPicker, FingerprintEnrollPanel, OnlineAccountRow) and are
|
||||
registered in `qmldir`, but nothing here has loaded the QML.
|
||||
- **The enrollment cancel path is pinned only from the source.** The canned
|
||||
fprintd has no main loop, so `SIGTERM` releasing the device is exercised by
|
||||
neither half; what is pinned is that the release happens in a `finally` and
|
||||
that a *failed* enrollment still releases.
|
||||
- **`user-accounts-contract`'s first half reads the live account service**
|
||||
(`panama-users snapshot`, `stock-avatars`), so it wants the same quiet moment
|
||||
the other system contracts do, though nothing it does writes.
|
||||
- **Seams pinned by name**: `PANAMA_FINGERPRINT_FIXTURE`/`_LOG`,
|
||||
`PANAMA_ACCOUNTS_FIXTURE`/`_LOG`, and the log shapes
|
||||
(`{method, arguments}` and `{verb, arguments, passwordBytes}`). Changing
|
||||
either is meant to be a deliberate act that updates these contracts.
|
||||
- Run order for this phase: the hermetic ones first (`fingerprint-contract`,
|
||||
`online-accounts-contract`), then the source-only ones
|
||||
(`gnome-handoff-contract`, `setup/readme-contract`,
|
||||
`settings-ownership-contract`, `search-routing-contract`), then the read-only
|
||||
system one (`user-accounts-contract`), then the harness ones
|
||||
(`settings-search-contract`), and `settings-pages-contract` last, as before.
|
||||
|
||||
Reference in New Issue
Block a user