Repository navigation
feat(identity): one Builderlab sign-in, the native NIP-FI session - #2576
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4e64d85c6b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for server in host.servers() { | ||
| snapshots.push(server.restore().await); |
There was a problem hiding this comment.
Restore independent sign-in servers concurrently
Each restore() may spend the federated protocol's 30-second deadline verifying a saved session, but this loop waits for every configured server serially. Consequently, an unrelated offline server delays signInServers() consumers such as the first-run hosted-community decision and Settings by another full timeout per saved server (up to the configured server limit). These sessions have independent stores and operation locks, so restore them concurrently while preserving the returned configuration order.
Useful? React with 👍 / 👎.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Browser fixture migration and the primary-server sign-in control need repair. The existing restore/sign-out recovery finding also remains unresolved.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One documentation contract mismatch in the header repair; existing findings remain unresolved.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Additional full-review finding.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Additional full-review finding.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Additional full-review finding.
| if (raw === "") return undefined; | ||
| // The development broker keeps its own Builderlab sign-in unless the | ||
| // build adds a server (native's `dev_viewer`). | ||
| if (raw === undefined && live) return undefined; |
There was a problem hiding this comment.
🤖 [P3] Reject userinfo before accepting a built-in Builderlab URL
With FI unset, BUZZ_BUILDERLAB_URL=https://u:p@app.builderlab.xyz matches the built-in origin despite containing userinfo. The native built-in branch bypasses the legacy target validation that rejected credentials, while Vite returns the raw URL into VITE_BUZZ_BUILDERLAB_URL. Reject username/password in both built-in checks, or return the canonical constant rather than the raw configuration; cover this configuration in shared fixtures.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Additional onboarding finding.
Every build signs in to its Builderlab with NIP-FI, and every Builderlab feature reuses that one native session from Keychain: onboarding, the Builderlab plugin, hosted agents, communities, Decisions, Apps and key backup. The built-in public server is FI-only and the legacy browser login, its saved credential and the multi-source backup handoff are removed. Squashes #2341 (NIP-FI sign-in in every build) and the one-login work onto main, adapting main's backup consent (#2378) and linked key read (#2486) to the FI session. Co-authored-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…he viewer exchange Kgoose's /v1/auth/hosted-site/native-session accepts the native CLI session only in X-BB-Session-Credential and returns 403 when any Authorization header is present. The NIP-FI session token is that CLI session (NIP-FA wraps the CLI login), so send it there instead of as a Bearer token. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Signed-off-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Three places described sign-in behaviour the code no longer has:
- A build with no sign-in server does not fall back to the built-in
server's communityHosts; it hosts none and sends a found community to
Enter your private key, not Connect to {host}. Only native reads
built-in-server.json.
- The FI-session paragraph now names each call's header and
destination (X-BB-Session-Credential for the Apps viewer exchange,
the Apps control plane for agent tools, Bearer elsewhere), key
recovery's separate sign-in, and scopes the 401 recheck to API and
Decisions calls.
- A build with another BUZZ_BUILDERLAB_URL has no Builderlab sign-in
outside live development (config.rs and .env.example).
No behaviour changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
ef3bafe to
42ccede
Compare
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One documentation correction; previously reported runtime findings remain unresolved.
Native Buzz kept one loopback callback slot for the whole app. All NIP-FI
sign-in servers and key restore shared it. In builds with two or more
sign-in servers, starting a sign-in to one server while another server's
browser sign-in was pending failed at once as browser-unavailable ("Sign-in
did not finish. Try again."). It kept failing until the first attempt
ended, up to 10 minutes later. Each attempt already binds its own
ephemeral port and random path, so the shared slot was only bookkeeping.
OAuthCallbackHost now keeps one attempt per owner key. FI passes its
server fingerprint and restore passes "nsec-restore". Attempts from
different owners are pending at once and settle independently. Within an
owner nothing changes: a second attempt is refused while one is pending,
a stale ID still cannot cancel a newer attempt, and a main-document load
still clears every attempt. The README's attempt rules now describe one
slot per owner.
Tests cover cross-owner independence, same-owner refusal next to
cross-owner admission, reload clearing two owners, and a peer server's
attempt surviving another server's cancel and replacement. The three
cross-owner tests fail when the host is made to refuse any pending
attempt, and pass with this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Buzz copies of one debug/release flavour share each sign-in server's Keychain item, whatever their app identifier, like the human key. That sharing stays. But a running copy read the item only once, and its compare-and-swap writes then failed whenever another copy had written since. With Buzz and Buzz Staging both using the public server, or two debug worktrees: - Sign-in in the stale copy failed with "Sign-in did not finish. Try again." after a full browser trip, and kept failing until restart. - Sign-out in the stale copy showed "Couldn't sign out of ...", although the session was already gone, and its Try again could delete the other copy's newer session. A signed-out copy now reads the item again before a browser sign-in. If another copy saved a session, it restores and verifies that session instead, with no browser. An empty item or a failed read starts the browser sign-in as before. A sign-out whose removal finds the session already ended or replaced now finishes signed out. The stale record's refusal marker stays, so a record put back later cannot restore, and the newer session is left alone. No migration: service, account, index folder and the Sign out of Buzz wipe are unchanged. The store and docs/identity.md now say the session is shared across copies, and docs/buzz-help.md tells people what that means for sign-in and sign-out. Tests cover adopting another copy's session at sign-in and a stale sign-out (session emptied or replaced); both fail without this change. A store test pins that a stale removal keeps its refusal marker and the newer session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Shared-session reconciliation still loses sign-out and account-switch intent. Two reproduced P2 defects and a statically traced renewal gap are detailed inline.
| state.restored && state.session.is_none() | ||
| }; | ||
| let attempt = | ||
| if signed_out && matches!(self.storage(|store| store.read()).await, Ok(Some(_))) { |
There was a problem hiding this comment.
🤖 [P2] Preserve explicit account-switch intent when adopting a shared session
If copies A and B restored S and B renews it to S′, A's Switch account first signs out. Conditional removal sees session-changed, leaves S′, and reports success. The subsequent sign-in now adopts S′ without opening the browser. If the community refused that account, it returns to the same Switch account prompt. A native probe confirms SignedOut followed by SignedIn with S′ and only GET /v1/session, with no login request. Track the revision deliberately left behind or provide a browser-forcing switch path so adoption cannot undo explicit switching. Also narrow the shared sign-out claim in buzz-help.md:79–83.
Two browser fixtures still assumed the removed legacy Builderlab login. They fail 22 browser cases, chromium and webkit, and keep CI required red. - The Apps fixture imported the deleted oauth/session module, so it never loaded. That failed all 7 Apps cases in each engine. It now builds the real createNativeSession over a modeled signed-in NIP-FI session that answers GET v1/auth/me. The fixture waits for the account read before applying Apps, so the first attach is still the authenticated one. - The compiled browser build is not an FI build, so the Builderlab plugin no longer applies and Settings has no Builderlab section. A new worker-scoped federatedIdentity build option defines VITE_BUZZ_FEDERATED_IDENTITY=1. Only the four journeys that open that section use it: avatar Settings access, the two hosted deletion journeys and the hosted setup cards layout. With no native host, the session stays signed out and hosted communities use the fixture broker, the surfaces these journeys had before. Playwright rejects a worker-scoped use() inside a describe, so the journeys opt in through test.extend, like confirmedPresence. new-message.spec.mjs declares the new option next to the other build options, because buildApp now depends on it. No assertions change. The other diff lines are formatter re-indentation. Before, at 17a82a6: those 22 cases fail in chromium and webkit, the same failures as CI. After: they pass, builderlab-session.spec.mjs (default build) passes in both engines, and so do the full settings.spec.mjs, settings-layout.spec.mjs and new-message.spec.mjs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Settings showed every unavailable sign-in server as "Couldn't sign out
of {server}", with a Try again that signs out. A restore that failed,
offline, on a server error or on a denied Keychain prompt, is also
unavailable, but its stored session may still be valid. One click
deleted it and retired that server's agent grants. Since 17a82a6,
Sign in in a signed-out copy could lead there too: a network failure
while adopting another copy's session showed the same alert, and Try
again removed the other copy's record.
The expiry, 401-recheck and assertion-denial cleanups also reported a
session-changed removal as unavailable. In a stale copy whose old
session expired after another copy saved a newer one, Try again
deleted the newer session.
Native snapshots now carry `cleanup`. It is set only when removing a
retired session's record failed, and every other transition clears it.
The three retired-session removals and sign-out share one helper. A
session-changed removal leaves the state as it is (expired, or signed
out), keeping the refusal marker and the newer session, as sign-out
already did. Settings lists a server as an unfinished sign-out only
when `cleanup` is set. A failed restore of the build's Builderlab now
shows its recovery text with no controls; a peer's shows nothing, like
a signed-out peer.
Tests: a restore that fails upstream keeps its record without
`cleanup`; a failed sign-out sets it; an expired session in a stale
copy ends expired and leaves the newer record, while a failed removal
sets `cleanup`. In Settings, a peer's failed restore offers no Try
again and never signs out, and the first server's shows only its
recovery text. The two unfinished sign-out tests now publish `cleanup`.
All four Rust tests (with the snapshot's field count) and both new
Settings tests fail against the unfixed code and pass with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
… peer Settings → Identity → Organization sign-in showed the build's Builderlab row only while no server was signed in. Once any peer was, it listed just the signed-in rows, so the first server's Sign in, its expiry notice, Cancel sign-in and recovery text all went away. In a Block build, bb-block signed out or expired beside a signed-in bb-public (after Not now at launch) left no way to sign in to Block from Settings, although the help guide says sign-in lives there. Block builds compiled only bb-block before this PR, so their row always showed. The multi-server list now starts with that row unless the first server is signed in or has an unfinished sign-out, which already shows below the list. OrganizationSignIn renders only its row when `listed`, titled by server the way signed-in rows are, so it reads correctly beside "app.builderlab.xyz". The list keeps one section and one heading, and the single-server output is unchanged. Listed and unavailable, the row names its server and shows its recovery text with no control. serverTitle() now gives both rows their title. The identity doc and help guide say the build's own server stays listed until you sign in to it. Tests: Block expired beside a signed-in public server shows the Block row with Sign in and the expiry notice, the public row with Sign out, and one Organization sign-in heading; Sign in calls only Block's session. Block's failed restore beside it shows the named row and its recovery text, with no Sign in or Try again. Both fail without this change and pass with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Buzz copies that use the same sign-in server share its Keychain item. A signed-in copy saved a renewal only if the item still held the session that copy had restored. If another copy had renewed or signed out since, the renewal went through the whole browser trip and then failed with "Sign-in did not finish. Try again." Every retry failed the same way until a restart or until the old session expired, and each one left an unsaved session live at the server. In practice this hits two debug copies built with the same at-launch server (review thread 4480578889). A completed browser sign-in, renewal or not, now reads the item and saves over whatever session it holds. If another copy writes between that read and the swap, the save still fails as session-changed. In-process ordering is unchanged: the operation lock and the revision checks stay. If this copy's session ends before the new one is installed, the undo puts back the record the save actually replaced, which may be the other copy's. Removals stay conditional, so a stale sign-out, expiry or refusal still leaves a newer session alone. The Switch account fix for thread 4480578877 also needs this save. Tests: a renewal saves and installs its session when the item holds another copy's record, and when it is empty. Before this change both cases failed with session-changed. A renewal ended after saving over another copy's record puts that record back. an_abandoned_switch_withdraws_only_its_own_retirement made the save fail with an in-memory record that didn't match the item, and that save now succeeds. It now fails the save with a storage error (storage-denied), and every other assertion is unchanged. docs/identity.md now says a completed browser sign-in, including a renewal, saves over whatever session the item holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Since 17a82a6, a signed-out copy reads the shared Keychain item before a browser sign-in, so it can adopt a session another copy saved. That read awaits between sign-in's revision check and the restore that adopts the session, and nothing rechecked the revision after it (review thread 4480578864). A sign-out started during the read published signed out, then waited on the operation lock. The sign-in still restored and installed the other copy's session, under the post-sign-out revision. The sign-out then removed that session from the item and returned signed in. The app stayed signed in after Sign out, with an empty item, until the next sign-out or a restart. The UI never overlaps a sign-in and a sign-out for one server, but two IPC calls can. - After the read, whatever it returned, sign-in rechecks close and the revision in the same lock that marks the shared session for restore. A sign-out during the read now also stops a browser sign-in starting. - A sign-out bumps restore_attempt, so a restore begun before it is refused at admission. That closes the lock-free gap between that lock and the restore taking its revision, when the sign-out runs on another worker thread. The bump must share the retirement's lock: in a later lock, a restore could take the new revision and still be admitted. So begin_sign_out retires through retire_state, and its account reset and retirements bump join that lock, as the retirements field doc already says. retire is now test-only. Tests: - a_sign_out_during_the_shared_item_read_ends_signed_out holds sign-in's read with a new one-shot Memory read_gate, signs out, then releases it. Both calls end signed out, the item is empty and the other copy's record is never verified. - a_sign_out_refuses_a_restore_begun_before_it signs out between taking an attempt and restoring. The restore is refused without verifying. Both fail without the production change (the first ends signed in, the second installs the session) and pass with it. Each also fails with only the other half of the fix, so each pins its own half. docs/identity.md already says sign-out fences stale work, so no docs change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Switch account signs out of the refusing community's server, then signs
in again. Clicking Cancel sign-in during that browser step reported
"Couldn't switch your {server} account. Sign-in did not finish. Try
again." (review thread 4477036255). The sign-out had already retired the
session, so native cancel_sign_in ends signed out with no failure code,
and runSwitch treated any non-signed-in result as a failed sign-in step
with an empty code.
A switch's sign-in that resolves signed out with no failure code now
counts as sign-in-canceled. SwitchProgress shows that as quiet status
text, "Sign-in was canceled. Sign in when you're ready.", with a Sign
in button. The button repeats the sign-in step and never signs out
again. A refused or failed step still names the server and offers Try
again. Closing the prompt instead would not do: the refusal view has no
Sign in for a signed-out server, and Settings lists no row for a
signed-out peer, so this prompt is the only way back in.
The switch-journey test pinned the old copy. It now expects the
canceled status, no alert, no "Couldn't switch" and no Try again, and
checks that Sign in calls signIn again without a second signOut. It
fails against the unfixed component and passes with this change.
docs/identity.md and docs/buzz-help.md describe the canceled state.
The thread's second part, a retry while the original sign-in is still
pending after cancelSignIn rejects, is unchanged. Native already refuses
the overlapping attempt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
On main, a public build's Builderlab sign-out sent POST
{Builderlab}/api/goose/v1/auth/logout with the session in
X-BB-Session-Credential. This PR moved those builds to the native NIP-FI
session, whose sign-out only removed it from Keychain (review thread
4477036022). The token, a kgoose CLI session, then stayed valid at the
server for up to 30 days, with its Apps viewer sessions, and any copy of
it kept working, including in another open Buzz copy sharing the item.
finish_sign_out now starts that logout for the session this copy held in
memory, after the removal and whatever the removal found. It is one
detached POST to {identity origin}{API base path}v1/auth/logout, with the
session in a sensitive X-BB-Session-Credential and no Authorization or
Cookie, through protocol::client() (no redirects, 30 s timeout). The
result is ignored and nothing retries, so sign-out still returns at once
and works offline. Kgoose's route revokes the CLI session given in that
header and answers with a redirect to its identity provider, which is not
followed. A sign-out with no session in memory revokes nothing, because
the record it reads back may be another copy's live session. Sign out of
Buzz (close), expiry, the 401 recheck and assertion refusal revoke
nothing. Both the plain sign-out and AgentTokens::sign_out reach it.
Tests use loopback fixtures only. Inner keeps a test-only list of the
detached logouts so tests can wait for them:
- Sign-out returns before the server answers. Exactly one POST
/api/v1/auth/logout arrives, with the session in the header and no
Authorization or Cookie. A 302 to another listener is not followed and
a 500 is not retried (either would wait on a listener that never
answers), and the app stays signed out with no failure.
- After another copy ended or replaced the session, only this copy's
token is revoked, never the newer one.
- AgentTokens::sign_out revokes too.
- Signing out of one server revokes only its session. Its logout target
is unreachable, which changes nothing.
- Local expiry and close() revoke nothing.
signing_out_of_one_server_keeps_the_others_session now points both
servers at an unreachable loopback origin, so it never contacts
Builderlab. The local signed_in_at helper also serves the identity origin
from its base. Other existing sign-out tests whose identity origin is a
reserved .example name now start a logout that fails at name lookup.
With the revoke call removed, the four positive tests fail, and they pass
with it. Following redirects, retrying a 500 or adding Authorization each
make the main test fail.
docs/identity.md describes the best-effort revoke. docs/buzz-help.md says
an open copy sharing that sign-in asks to sign in again the next time it
uses it, and keeps it until restart if the server couldn't be reached.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
A build with no Builderlab sign-in server has no `connect`, so a community found at first run goes straight to Enter your private key. That page's Back still sent a user with no account to the connect choice (review thread 4477037641). It offered Sign in with Builderlab, which this build can't run, and clicking it ended on "Couldn't sign in" with "This Buzz build has no Builderlab sign-in." With no account and no `connect`, Back now returns to the landing, as the connect choice's own Back does. The typed address is kept for Other options. Builds with a sign-in server are unchanged, and so is Back on the account lanes. Only builds that compile no sign-in server reach this: the CI-only FI opt-out, `just desktop` with a pinned BUZZ_DEV_VIEWER, and local builds with another BUZZ_BUILDERLAB_URL. No shipped build does. Test: rendering Welcome with no `connect` and no sign-in servers, a found community opens Enter your private key, and Back shows the landing with no Connect page and no Sign in button. Other options still holds the address, with no second lookup. It fails without this change (Back lands on Connect to your community) and passes with it. docs/identity.md says where that Back goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
42ccede said a build without a sign-in server "has no Builderlab sign-in". That includes `tauri dev` with the development broker pinned, where the Builderlab plugin still signs in through the broker (brokerBuilderlab() in dev-session.ts, used by the plugin's apply()). Only first run has no sign-in there: with no compiled server, Welcome gets no `connect`, and a found community opens Enter your private key. The same doc's FI-session paragraph already lists the broker's sign-in in live development (review thread 4480111059). - docs/identity.md: such a build has no first-run Builderlab sign-in. In live development the plugin still signs in through the broker, unless BUZZ_BUILDERLAB_URL is empty, and first run never uses that sign-in. The Connect page sentence says the same. - .env.example and docs/configuration.md said "except that live development signs in through the development broker", which also covered an empty value. With an empty value the plugin's oauthTarget() throws "not configured", so the broker has no Builderlab to sign in to. The exception now covers only another origin and names the plugin; the welcome screen never offers it. - config.rs: the compiled() comment says the plugin keeps the broker's sign-in and first run never uses it. No behaviour changes. Checks: git diff --check, rustfmt --check on config.rs, and the staging descriptor that config.rs's test reads from docs/configuration.md is unchanged. cargo test was not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 requesting changes for four things. Thufir and I reviewed this at 15f984c289 from the source only, with no live run yet.
-
first run can continue as another Buzz copy's account without asking. native sign-in adopts a session another copy saved, without opening the browser, when this copy started signed out (
signing_in_adopts_a_session_another_copy_saved). first run only forces the browser account choice when the previous state wasunavailable, notsigned-out: first-run.ts#L109-L117. so the user lands in account/key setup as the other copy's account without choosing it, which contradictsdocs/identity.md:22-27. could the signed-out case get the same renewal, keeping the unchanged-revision cancel check? a test for signed-out, then shared-session adoption, then browser renewal (including a canceled renewal) would cover it. -
"Use a different account" doesn't sign the account out. on the account-choice step it calls
backToLanding(Welcome.tsx#L764), which only clears the welcome screen's credential vialogin.cancel()(Welcome.tsx#L518-L533). the native session stays signed in and the Builderlab plugin keeps using it. if you reject account A and then import a key for a self-hosted community, known-community sync can still run as A and send communities you join later to A. that exit needs a real native sign-out, with error handling and a guard so it can't retire a newer session. plain Cancel/Back can stay as is. the test "Use a different account … signs the session out" (identity.test.tsx:2131) only checks local state, so it should also check the shared session. -
operation()returns the latest pushed snapshot, not the invoke result: service.ts#L168-L179, copied intoCommunitySession. native publishessigning-inmid-call, which bumps the epoch, so the invoke result is always dropped and the caller gets whatever event arrived last. if the finalsigned-in/signed-outevent hasn't landed when the invoke resolves, Switch account'srunSwitchreports "Couldn't switch your account" after a successful or canceled sign-in, andfirst-run.ts:123throws. this depends on Tauri's event-vs-response ordering, so I'm not certain it fires in practice. returning the invoke's own snapshot (and onlyaccept()ing it when the epoch is unchanged) removes the dependency. a test where asigning-inevent arrives before the invoke resolves would pin it. -
key recovery still runs its own legacy browser login. "Restore your key" starts a PKCE
/v1/auth/login?type=clilogin (nsec_backup.rs#L680-L700) and uses the resulting Builderlab credential asX-BB-Session-Credential(nsec_backup.rs:1035). that's a second Builderlab sign-in outside NIP-FI, so the app still has two. a NIP-FI sign-in doesn't need a Nostr key, so recovery should be able to use the native NIP-FI session asAuthorization: Bearerlike backup does, and the legacy login path can go.
…ults First run renews a session adopted from another copy when starting signed out, so the browser always chooses the account. Use a different account signs the shared native session out unless a newer one replaced it, and reports a failed sign-out. FI operations answer with the invoke's own snapshot instead of whatever state an earlier event left. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <wpfleger@block.xyz>
|
🤖 dropping item 4 (key recovery on the NIP-FI session) from my review. kgoose's recovery route ( |
Signed-off-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Two first-run failure paths remain.
| // browser chooses. | ||
| if ( | ||
| before.status === "unavailable" && | ||
| (before.status === "unavailable" || before.status === "signed-out") && |
There was a problem hiding this comment.
🤖 [P3] Renew adopted sessions from reauthentication-required too
When this copy’s session expires and another copy saves a new session, native sign_in can adopt it from reauthentication-required: its predicate is restored && session.is_none() (federated_identity/mod.rs:768–783). This guard excludes that state, so first run accepts the adopted account without the promised browser account choice. A targeted probe starting in reauthentication-required observes one signIn call and the other copy’s credential; native reachability is source-traced. Cover every start without a live session when the result is signed-in and no browsing occurred, and add this state to the adoption regression cases.
…e returned session Native sign-out takes an expected revision and compares it in the lock that retires the session, undoing the grant-retirement record on a mismatch, so a newer session and its grants survive. First run waits for the pushed snapshot to reach the session sign-in answered with before reading the account. A sign-out that resolves incomplete (cleanup) counts as a failure, and Try again retries it with the same revision fence. Switch account reads sign-out's own answer. Signed-off-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Signed-off-by: Will Pfleger <wpfleger@block.xyz>
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One new cleanup-recovery defect remains.
… never mark a stale sign-out Try again after an incomplete sign-out retries fenced to the revision native answered with, since retiring moves it. First run also waits while the pushed snapshot is still a session from before its sign-in, so a renewal whose event lags its answer continues. A revision-fenced sign-out that no longer matches returns before writing the grant-retirement marker. Signed-off-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Bring in bundled Drafts & sent (#2644), the Builderlab sign-in (#2576) and the changes before them. Drafts & sent is a sidebar page, so it lists in the Messages sidebar rather than the dock; help says so. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Signed-off-by: Bradley Axen <baxen@squareup.com>
…leanup * origin/main: fix(dialog): limit footer click-through to the gradient gutter (#2545) fix(channels): put the huddle launcher after the other header tools (#2660) feat(drafts-sent): bundle Drafts & sent for everyone (#2644) feat(identity): one Builderlab sign-in, the native NIP-FI session (#2576) fix(agents): let people dismiss connection setup cards (#2631) Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Signed-off-by: Will Pfleger <wpfleger@block.xyz> # Conflicts: # .github/workflows/release.yml # crates/plugin-manager/src/lib.rs # docs/configuration.md # src/bundled/builderlab/apps/client.test.ts # src/bundled/builderlab/apps/environment.ts # src/features/identity/builderlab-hosted.test.ts # src/features/identity/identity.test.tsx # vite.config.ts
…-service * origin/main: fix(sidebar): highlight collapsed sections with unread channels (#2539) perf(threads): isolate thread rows inside the scroller (#2462) perf(messages): skip the glass on closed message action bars (#2461) perf(threads): measure long-thread scrolling in browsers and the macOS app (#2460) docs(threads): set scrolling requirements and targets for long threads (#2459) fix(composer): seed required recipients in saved briefs (#2667) fix: preserve agent activity ownership and remove Solo composer gap (#2613) feat(agents): open agent card links in Create agent (#2662) Align Bestie messaging with the shared conversation workspace (#2614) feat(messages): add a compact message density (#2547) fix(dialog): limit footer click-through to the gradient gutter (#2545) fix(channels): put the huddle launcher after the other header tools (#2660) feat(drafts-sent): bundle Drafts & sent for everyone (#2644) feat(identity): one Builderlab sign-in, the native NIP-FI session (#2576) fix(agents): let people dismiss connection setup cards (#2631) AIDA-2425: Preserve links beside pasted images (#2520) feat(activity): show reactions to your messages in Activity (#2638) # Conflicts: # docs/identity.md # src-tauri/src/federated_identity/agent/mod.rs # src-tauri/src/federated_identity/mod.rs # src-tauri/src/federated_identity/tests.rs # src/bundled/codex/runtime.ts
Rebased onto main after #2576 (one Builderlab sign-in). Combines this branch's onboarding work with main's native NIP-FI session: - Create account and Sign in on the landing; Create account opens the sign-up screen, and Other options keeps joining by community address. Builds without a Builderlab sign-in show only Other options. - After sign-in, setup finishes on its own: an account with no key uses old Buzz's identity from this device when there is one, otherwise a new key; Welcome back only for a key that is elsewhere. - Use a different account signs the first-run session out through main's revision-fenced release, and every sign-in after it asks the browser for credentials until one completes. - A sign-in that times out waiting on the browser shows as one to finish, and Open Buzz restarts it. - Connect to a community, glass buttons, and success/danger text updates. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Bradley Axen <baxen@squareup.com> Co-authored-by: Morgarita <6e967af659416160ab4f2fd56d74c8c6f273791ee7bfc9468a1be37c387947be@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
App navigation moves to a left dock, and the header keeps only history, Search and window controls. ### What changes - **Dock.** Bestie, Activity, Messages, Reminders, Agents, Workflows and any plugin page placed in the topbar or toolbar are icon tabs in a left column, Bestie first. Labels show at once, as a documented dock label hover bucket, and on keyboard focus; inactive Activity keeps its hover preview and shows the latest reaction emoji. With a plugin shell backdrop, the dock keeps the same solid floor as header controls. The community picker and profile sit at the bottom of the dock, and their menus open to the right. - **Header.** Back/forward, a centered Search capsule with its shortcut (⌘K by default, follows rebinding), and window controls. Search fills the space at smaller widths and drops to its own row when narrow. The old header page tabs and "More pages" overflow are removed. - **Sidebar only on Messages.** Pages without their own sidebar (Activity, Agents, Bestie, Usage, Settings…) use their main view. Sidebar-placed pages (Usage, Drafts & sent) list in the Messages sidebar. New and ⌘N follow the creation sidebar. - **Conversation headers.** Member avatars and the conversation name form one button that opens the existing access dialog, named after what that dialog offers: "Manage members of …", or "Share access to …", "Chat access for …" or "Channel access for …" for Solo chats, visitors and guests. Solo chats show their agents. A conversation with one tab no longer shows a tab strip. - **Design system.** `Tabs` gets a `dock` variant and `orientation`, `Tooltip` gets `hint="dock"`, and `PanelHeader`'s divider changes for every panel header. - **Also:** macOS traffic lights move to 16,24; message history fades at its top and bottom edges (profiled: no frame cost in WebKit or Chromium, ~18% more scroll CPU in headless Chromium); Activity row avatars change size. ### Since the first draft - Rebased once onto main, then merged main up to `f6b0c2c62` (Activity rename #2543, reactions in Activity #2638, Drafts & sent #2644, Builderlab sign-in #2576). - Restored the Search shortcut hint. `PageNavigation` takes `placement="dock"` and labels its tablist "Apps". `ChannelHeaderIdentity` owns the conversation avatars, label and hint. Header measurement uses refs. Removed dead attributes, keys and a duplicate check. - Browser tests from main updated for the dock: the Activity reaction emoji is on the dock tile, Manage members is named by its conversation, and Activity toolbar widths leave room for the dock. - Merged main again, up to `fd6296a5c`. Then review fixes: dock labels show on keyboard focus, and the dock label hover bucket is documented. The header access names match their dialog. The sidebar grip's backdrop halo is gone, since the grip now sits on the opaque joined seam. The Search shortcut chip no longer makes the topbar 48px tall. Test fixes for the shell backdrop, wrapped link drag and Drafts & sent placement checks. ### Validation (on `92fb0abe6`) - `pnpm check` and pre-push hooks pass. Full vitest: 18,294 pass, 1 fails: `ChannelMembersDialog.test.tsx` "drains a gated action … after cancellation", which fails the same way on a clean main checkout on this Mac and passes in isolation. - 248 browser tests across the layout, Activity, page placement, backdrop, channel settings, sidebar, Solo, lifecycle, permissions and profile specs, plus the wrapped-link and zoom tests, pass in Chromium and WebKit. Integration tests (261) passed before the last two merges from main. The full browser suite runs in CI. - One local failure, `layout.spec.mjs` "Alt+ArrowUp from a message link preserves native scroll ownership", fails the same way on a clean main checkout on this Mac. - Native rendering, traffic lights, window dragging and real sign-in were checked by hand in the desktop app. --------- Signed-off-by: Arjun Mahanti <arjun@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: bmo <a7b504d9a2bfe905589e9ac237d7d8d023b8d10547060ec9618e6487b55d756c@buzz.block.builderlab.xyz> Co-authored-by: Codex <codex@openai.com> Co-authored-by: Bradley Axen <baxen@squareup.com> Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
Rebased onto main after #2576 (one Builderlab sign-in). Combines this branch's onboarding work with main's native NIP-FI session: - Create account and Sign in on the landing; Create account opens the sign-up screen, and Other options keeps joining by community address. Builds without a Builderlab sign-in show only Other options. - After sign-in, setup finishes on its own: an account with no key uses old Buzz's identity from this device when there is one, otherwise a new key; Welcome back only for a key that is elsewhere. - Use a different account signs the first-run session out through main's revision-fenced release, and every sign-in after it asks the browser for credentials until one completes. - A sign-in that times out waiting on the browser shows as one to finish, and Open Buzz restarts it. - Connect to a community, glass buttons, and success/danger text updates. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Bradley Axen <baxen@squareup.com> Co-authored-by: Morgarita <6e967af659416160ab4f2fd56d74c8c6f273791ee7bfc9468a1be37c387947be@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Onboarding audit for desktop first run: account-first Builderlab sign-in, recovery for a stalled sign-in, and clearer community setup. - **Sign-in recovery.** A sign-in that times out waiting on the browser shows "Finish in your browser" with Continue, not "Sign-in did not finish". Open Buzz (`buzz://sign-in`) restarts a waiting or timed-out sign-in, including one started from a community address. - **Connect to a community.** It's a join-or-create choice, and it only offers "create your own" when Create is available. A refused create replaces "available" and holds Create until the name changes. - **Dark-mode hover fill.** Hover fills stay light on light surfaces inside dark mode, so "Other options" is no longer dark text on a dark fill. - **Clear glass button.** Rebuilt on the shared primary glass material. **Account details (name, birthday, country) are not in this PR.** The in-app form was reverted in the last commit. Public desktop sign-in now goes through the hosted setup page mobile uses: [ext-builderbot-ui #403](squareup/ext-builderbot-ui#403), then [cash-server #146925](squareup/cash-server#146925). Overlaps #2576 in `federated_identity/mod.rs` and `oauth_callback`, so it rebases onto #2576 once that lands. Checks at the head commit: `tsc` clean, biome clean, full vitest run 17,857 passed / 22 skipped. --------- Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Bradley Axen <baxen@squareup.com> Co-authored-by: Morgarita <6e967af659416160ab4f2fd56d74c8c6f273791ee7bfc9468a1be37c387947be@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…-in (#2222) ## Problem When you run Buzz as a browser build in live development, Builderlab sign-in goes through the dev broker (`browser-host/builderlab.mjs`). The broker still sends `product=builderlab` on `/v1/auth/login`, so the hosted login shows the generic Builderlab page. The shipped desktop app shows the Buzz page ("continue to Buzz"). ## Change The dev broker now sends `product=buzz`, so browser-build testing matches the desktop. The desktop sign-in was originally part of this PR. On main, #2576 moved it into native code (`nsec_backup.rs`), and that code already sends `product=buzz`. This branch now takes main's version of those files. ## Safety This is development-only. Shipped builds don't use the broker. `product` doesn't affect account access. kgoose checks it against an allowlist and forwards it as `ext-product`. Neither the access policy nor the callback check reads it. Only the hosted login pages in ext-builderbot-ui use it, to pick the Buzz or Builderlab look. ## Screenshots This PR doesn't change any Buzz UI. The only visible effect is the hosted login page during browser-build development. ## Testing - `vitest run browser-host/builderlab.test.mjs` (53 passed, after merging main) - `biome check` passes on the changed files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Carl <c217fe6b9d958f41c3a5e030dccc7f626775a923089cb6491305eade75ea1f1b@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Morgarita <6e967af659416160ab4f2fd56d74c8c6f273791ee7bfc9468a1be37c387947be@buzz.block.builderlab.xyz>
Summary
Buzz signs in to Builderlab once, with the native NIP-FI session. Native code keeps the token in Keychain and never hands it to the JS side. Everything that uses Builderlab reuses that one session: onboarding, the Builderlab plugin (New agent → Builderlab, model catalog, Identity), hosted communities, remote agents, Decisions, the Apps agent tools, the Apps viewer and key backup. The old in-renderer browser login is deleted. Key recovery is the one exception: it keeps its own separate native browser sign-in, because kgoose's recovery route only accepts a CLI login session.
Supersedes #2498.
What it changes
createOAuthSession, thelegacysession plumbing, theoauth_callback_*Tauri commands, and the legacy credential in Decisions IPC and nsec backup (backup now sends only Bearer). main's styled callback pages stay; the browser-focus return (focus.rs) is removed.X-BB-Session-Credentialremains only where kgoose takes nothing else: the Apps viewer exchange sends the FI session in it, sign-out's server revoke sends it tov1/auth/logout, and key recovery uses it with its own CLI session./v1/agent/*goes to compose-ctrl withAuthorization: BBIdentity <session>, and the deploy upload is checked natively.requires_nip_fi.Sign-in and sign-out behavior
POST v1/auth/logout, no redirects followed). A sign-out that can't finish removing the stored record shows as a failure with a working Try again.FederatedIdentityServicereturns each command's own result rather than the latest pushed snapshot, so a late event can't turn a successful sign-in into a failure.Behavior changes
BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG="".docs/configuration.mdhas a bb-public staging descriptor ready to paste.Server dependency
Prod now serves the sign-in routes, since bbpci #4069 merged:
app.builderlab.xyz/api/goose/v1/sessionreturns 401 and/v1/login/startreturns 400 on an empty request. Create account doesn't sendscreen_hint=signupyet; that comes in a follow-up.