Request permission for changing the profile #140

Closed
opened 2026-08-21 13:51:09 +00:00 by Grandiras · 5 comments
Owner

We want all people to be able to change their profile on Ponente, so we need to request that permission. Only hacking this together for our own PDS is not the right approach. Also, we should make clear, that this will change the network-wide profile. But please don't write a whole paragraph for that, just a short, concise explainer. This type of information density is actually a theme we should put into agent instructions too, as it happened too often that it got far too verbose.

We want all people to be able to change their profile on Ponente, so we need to request that permission. Only hacking this together for our own PDS is not the right approach. Also, we should make clear, that this will change the network-wide profile. But please don't write a whole paragraph for that, just a short, concise explainer. This type of information density is actually a theme we should put into agent instructions too, as it happened too often that it got far too verbose.
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/364); the reply will link the full session transcript. <!-- claude-run:comment --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-140/run-364.md) — updating every ~5 minutes while the task runs.
Collaborator

Pushed claude/issue-140.

What changed

Ponente now asks for the permission. AtProto:ProfileScope defaults to generic (atproto transition:generic) instead of none, so every member can edit their own name and picture from /profile — wherever their account lives. The alternative you ruled out is ruled out for the reason you gave: writing the record with our PDS's admin credentials would work for members who signed up here and for nobody who arrived with a bsky.social account, which is a two-class board rather than a feature. The OAuth grant is the only mechanism that works for everyone.

transition:generic is still the only scope that works — I re-measured /.well-known/oauth-authorization-server on bsky.social and pds.ponente.app today, and both advertise ["atproto", "transition:email", "transition:generic", "transition:chat.bsky"]. The granular pair (repo:app.bsky.actor.profile blob:image/*) the spec defines for exactly this is still accepted nowhere, and asking for it breaks sign-in outright rather than degrading. granular stays as the one-line migration for the day that changes.

Unset and unrecognized are now different answers: nothing configured gets the default, while a value that is set and not understood (off, disabled, a typo) still reads as none — a mistyped variable must not hand somebody's account over.

The explainer, in one line. The network-wide warning moved from the top of the page to directly above the two fields it warns about, where someone about to press Save actually reads it, and it is one sentence: "This is your Atmosphere profile: what you save here is what every app on the network shows, not just Ponente." The permission footnote lost a sentence too. Nothing on the page says the same thing twice.

The profile page with editing enabled: the one-line network-wide explainer sits directly above the display name and picture fields

The density rule is written down — a bullet in CLAUDE.md's conventions and a new Voice section in docs/brand.md: say it once, in as few words as carry the meaning; an explainer is a sentence, not a paragraph; the long-form reasoning belongs in docs/ or a code comment, which are read by people who came to read. /profile is the worked example there.

Also: Changelog.Unreleased (both locales, now telling members to sign in once more), docs/auth.md's scope section, README.md, docker-compose.yml, deploy/docker-compose.prod.yml and both .env examples.

Verified

  • dotnet build and dotnet test green — 381 tests, including the updated ProfileScopeTests and DocumentationTests.
  • Ran the app for real (host Postgres, AtProto__ProfileScope unset) and confirmed it logs Sign-in asks for "atproto transition:generic".
  • profile.spec.js (updated for the new default) passes against that running app, along with board, prefs, changelog and i18n — one i18n test fails only because it hardcodes a localhost:8090 cookie URL and I ran on 127.0.0.1:5000; unrelated to this change.
  • The screenshot is the real page. The e2e sign-in stores no OAuth token by design, so to reach the editable state I temporarily stubbed a granted token through the token store; the markup, copy and layout are what the committed code renders, and that temporary endpoint is not in the branch.

One thing to do at deploy time

The prod compose default is now generic, but the server's own .env overrides it and currently pins ATPROTO_PROFILE_SCOPE=none. Change that line (or delete it) and run deploy/deploy.sh — a tag alone won't carry it. Everyone then signs in once more before the fields work; /profile says so and links straight back through sign-in.


⏱ 13m 16s · 86 turns · tokens: 127.5k in / 42.7k out (+7995.6k cache-read)
📜 Full session transcript
→ Opened PR: #142

Pushed **`claude/issue-140`**. ## What changed **Ponente now asks for the permission.** `AtProto:ProfileScope` defaults to `generic` (`atproto transition:generic`) instead of `none`, so every member can edit their own name and picture from `/profile` — wherever their account lives. The alternative you ruled out is ruled out for the reason you gave: writing the record with our PDS's admin credentials would work for members who signed up here and for nobody who arrived with a bsky.social account, which is a two-class board rather than a feature. The OAuth grant is the only mechanism that works for everyone. `transition:generic` is still the *only* scope that works — I re-measured `/.well-known/oauth-authorization-server` on `bsky.social` and `pds.ponente.app` today, and both advertise `["atproto", "transition:email", "transition:generic", "transition:chat.bsky"]`. The granular pair (`repo:app.bsky.actor.profile blob:image/*`) the spec defines for exactly this is still accepted nowhere, and asking for it breaks sign-in outright rather than degrading. `granular` stays as the one-line migration for the day that changes. Unset and unrecognized are now **different** answers: nothing configured gets the default, while a value that is set and not understood (`off`, `disabled`, a typo) still reads as `none` — a mistyped variable must not hand somebody's account over. **The explainer, in one line.** The network-wide warning moved from the top of the page to directly above the two fields it warns about, where someone about to press Save actually reads it, and it is one sentence: *"This is your Atmosphere profile: what you save here is what every app on the network shows, not just Ponente."* The permission footnote lost a sentence too. Nothing on the page says the same thing twice. ![The profile page with editing enabled: the one-line network-wide explainer sits directly above the display name and picture fields](https://git.grandiras.net/attachments/770a124b-053a-471f-90dd-3201fa57a537) **The density rule is written down** — a bullet in `CLAUDE.md`'s conventions and a new **Voice** section in `docs/brand.md`: say it once, in as few words as carry the meaning; an explainer is a sentence, not a paragraph; the long-form reasoning belongs in `docs/` or a code comment, which are read by people who came to read. `/profile` is the worked example there. Also: `Changelog.Unreleased` (both locales, now telling members to sign in once more), `docs/auth.md`'s scope section, `README.md`, `docker-compose.yml`, `deploy/docker-compose.prod.yml` and both `.env` examples. ## Verified - `dotnet build` and `dotnet test` green — 381 tests, including the updated `ProfileScopeTests` and `DocumentationTests`. - Ran the app for real (host Postgres, `AtProto__ProfileScope` unset) and confirmed it logs `Sign-in asks for "atproto transition:generic"`. - `profile.spec.js` (updated for the new default) passes against that running app, along with `board`, `prefs`, `changelog` and `i18n` — one i18n test fails only because it hardcodes a `localhost:8090` cookie URL and I ran on `127.0.0.1:5000`; unrelated to this change. - The screenshot is the real page. The e2e sign-in stores no OAuth token by design, so to reach the editable state I temporarily stubbed a granted token through the token store; the markup, copy and layout are what the committed code renders, and that temporary endpoint is not in the branch. ## One thing to do at deploy time The prod compose default is now `generic`, but the server's own `.env` overrides it and currently pins `ATPROTO_PROFILE_SCOPE=none`. Change that line (or delete it) and run `deploy/deploy.sh` — a tag alone won't carry it. Everyone then signs in once more before the fields work; `/profile` says so and links straight back through sign-in. --- ⏱ 13m 16s · 86 turns · tokens: 127.5k in / 42.7k out (+7995.6k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-140/run-364.md) → Opened PR: https://git.grandiras.net/Grandiras/Ponente/pulls/142
Author
Owner

@claude-bot I'm not happy with the generic scope. If there is a bug in our ATProto SDK, then please file it, but I know a lot of apps like stream.place, mu.social, standard.reader and several others, that successfully use granular scopes instead of relying on that awful generic one.

@claude-bot I'm not happy with the generic scope. If there is a bug in our ATProto SDK, then please file it, but I know a lot of apps like stream.place, mu.social, standard.reader and several others, that successfully use granular scopes instead of relying on that awful generic one.
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/367); the reply will link the full session transcript. <!-- claude-run:comment --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-140/run-367.md) — updating every ~5 minutes while the task runs.
Collaborator

You were right, and I was wrong for a checkable reason. Pushed to claude/issue-140 (updates PR #142).

What I got wrong

I read scopes_supported from /.well-known/oauth-authorization-server and concluded the granular pair was unsupported. That list only ever names the fixed scopes — repo:<nsid> is parameterized and unbounded, so there is nothing to enumerate. The metadata document cannot answer this question; the authorization endpoint can.

What I measured today

Test Result
PAR atproto repo:app.bsky.actor.profile blob:image/*bsky.social 201, request_uri issued
…then GET /oauth/authorize 200, consent screen, scope intact
Same against pds.ponente.app (our own PDS) identical
Control: atproto totally:bogus.scope 200, consent screen reading scope: "atproto"
Ponente's own /login/start?handle=…, ProfileScope=granular 302 → bsky.social consent screen, scope intact

The control matters: an unrecognized scope is silently dropped, not rejected. So my "it breaks sign-in for everyone" claim was wrong twice over — the worst case of asking narrowly is a member who gets told to sign in again.

Enforcement lines up too. In the PDS source, putRecord asserts create and update on the collection (the action-less scope grants all three), uploadBlob asserts the MIME type, and getRecord has no auth handler at all — which is why /profile's read half works even on none.

No SDK bug, so no issue filed

AtProtoScopes.Repo("app.bsky.actor.profile", RepoAction.All) + Blob("image/*") emit exactly repo:app.bsky.actor.profile blob:image/*, and the whole SDK login path carried them to a working consent screen. The bug was mine, in ProfileScope's reasoning.

The change

AtProto:ProfileScope now defaults to granular. generic stays only as the blanket fallback for an account server too old to parse the narrow pair — documented as a deliberate downgrade, not a default. none is still the opt-out, and a set but unrecognized value still reads as none.

Also corrected profile.permission.generic in both locales, which told members "no account server offers a profile-only permission yet". It now says this deployment chose the broad one.

docs/auth.md and ProfileScope's remarks now carry the measurement, the control, and the rule that produced the mistake — test the authorization endpoint, not the metadata document — so the next agent doesn't re-derive it the wrong way.

Verified

  • dotnet build + dotnet test: 381 passed.
  • Ran the app with nothing configured; it logs Sign-in asks for "atproto repo:app.bsky.actor.profile blob:image/*" — … the grant covers that and nothing else.
  • Drove a real sign-in from that instance to bsky.social's consent screen (table above).
  • Playwright against it: profile (3), plus board, changelog, i18n, prefs — 35 passed, no failures.
  • Not verified: the actual write with a granular token. That needs a real member's password at a consent screen, which I don't have. The scope strings and the PDS's assertions match, but the first live save is a manual check.

At deploy time

The prod compose default is granular, but the server's .env overrides it and still pins ATPROTO_PROFILE_SCOPE=none. Delete that line (or set it to granular) and run deploy/deploy.sh — a tag alone won't carry it. Everyone signs in once more before the fields work.

consent.png


⏱ 18m 32s · 83 turns · tokens: 116.7k in / 39.3k out (+6810.1k cache-read)
📜 Full session transcript

You were right, and I was wrong for a checkable reason. Pushed to **`claude/issue-140`** (updates PR #142). ## What I got wrong I read `scopes_supported` from `/.well-known/oauth-authorization-server` and concluded the granular pair was unsupported. That list only ever names the *fixed* scopes — `repo:<nsid>` is parameterized and unbounded, so there is nothing to enumerate. The metadata document cannot answer this question; the authorization endpoint can. ## What I measured today | Test | Result | |---|---| | PAR `atproto repo:app.bsky.actor.profile blob:image/*` → `bsky.social` | 201, `request_uri` issued | | …then `GET /oauth/authorize` | 200, consent screen, `scope` intact | | Same against `pds.ponente.app` (our own PDS) | identical | | Control: `atproto totally:bogus.scope` | 200, consent screen reading **`scope: "atproto"`** | | Ponente's own `/login/start?handle=…`, `ProfileScope=granular` | 302 → bsky.social consent screen, scope intact | The control matters: an unrecognized scope is silently **dropped**, not rejected. So my "it breaks sign-in for everyone" claim was wrong twice over — the worst case of asking narrowly is a member who gets told to sign in again. Enforcement lines up too. In the PDS source, `putRecord` asserts `create` **and** `update` on the collection (the action-less scope grants all three), `uploadBlob` asserts the MIME type, and `getRecord` has no auth handler at all — which is why `/profile`'s read half works even on `none`. ## No SDK bug, so no issue filed `AtProtoScopes.Repo("app.bsky.actor.profile", RepoAction.All)` + `Blob("image/*")` emit exactly `repo:app.bsky.actor.profile blob:image/*`, and the whole SDK login path carried them to a working consent screen. The bug was mine, in `ProfileScope`'s reasoning. ## The change `AtProto:ProfileScope` now defaults to **`granular`**. `generic` stays only as the blanket fallback for an account server too old to parse the narrow pair — documented as a deliberate downgrade, not a default. `none` is still the opt-out, and a *set but unrecognized* value still reads as `none`. Also corrected `profile.permission.generic` in both locales, which told members "no account server offers a profile-only permission yet". It now says this deployment chose the broad one. `docs/auth.md` and `ProfileScope`'s remarks now carry the measurement, the control, and the rule that produced the mistake — **test the authorization endpoint, not the metadata document** — so the next agent doesn't re-derive it the wrong way. ## Verified - `dotnet build` + `dotnet test`: 381 passed. - Ran the app with nothing configured; it logs `Sign-in asks for "atproto repo:app.bsky.actor.profile blob:image/*" — … the grant covers that and nothing else`. - Drove a real sign-in from that instance to bsky.social's consent screen (table above). - Playwright against it: `profile` (3), plus `board`, `changelog`, `i18n`, `prefs` — 35 passed, no failures. - Not verified: the actual write with a granular token. That needs a real member's password at a consent screen, which I don't have. The scope strings and the PDS's assertions match, but the first live save is a manual check. ## At deploy time The prod compose default is `granular`, but the server's `.env` overrides it and still pins `ATPROTO_PROFILE_SCOPE=none`. Delete that line (or set it to `granular`) and run `deploy/deploy.sh` — a tag alone won't carry it. Everyone signs in once more before the fields work. ![consent.png](https://git.grandiras.net/attachments/9317425b-d60b-4129-a340-b9c177322bb8) --- ⏱ 18m 32s · 83 turns · tokens: 116.7k in / 39.3k out (+6810.1k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-140/run-367.md)
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/Ponente#140
No description provided.