镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

feat(identity): one Builderlab sign-in, the native NIP-FI session - #2576

Merged
wpfleger96 merged 19 commits into
mainfrom
mbot/one-login-main
Nov 5, 2026
Merged

wpfleger96 merged 19 commits into
mainfrom
mbot/one-login-main

Conversation

@baxen

@baxen baxen commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • Deletes the PKCE browser login and createOAuthSession, the legacy session plumbing, the oauth_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-Credential remains only where kgoose takes nothing else: the Apps viewer exchange sends the FI session in it, sign-out's server revoke sends it to v1/auth/logout, and key recovery uses it with its own CLI session.
  • The Builderlab plugin's only grant is now the FI grant.
  • The native FI forwarder gains an Apps mode: /v1/agent/* goes to compose-ctrl with Authorization: BBIdentity <session>, and the deploy upload is checked natively.
  • The built-in bb-public server is FI-only. A relay gets an FI assertion only when it advertises requires_nip_fi.
  • Adds Block staging to the Apps control-plane maps.

Sign-in and sign-out behavior

  • The session persists in Keychain, so Buzz is still signed in after a restart. Buzz copies that use the same server share it: a signed-out copy adopts a session another copy saved, and a completed browser sign-in saves over the shared item.
  • Sign-out ends the session locally and asks the server to end it (one best-effort 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.
  • First run renews an adopted shared session through the browser so the user can choose the account, and waits for the session that sign-in actually returned, including across a renewal, before reading the account. A cancel or a newer session stops it instead.
  • First run's Use a different account really signs out the session it signed in as. Native fences that sign-out to its revision, so a newer session and its agent grants are left alone, and a stale sign-out never marks grants for deletion. The community sign-in's Switch account acts on its own sign-out's result.
  • FederatedIdentityService returns 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

  • Builds with no FI server have no Builderlab sign-in, for example BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG="". docs/configuration.md has a bb-public staging descriptor ready to paste.
  • Release builds keep their config. Public builds get the built-in server, and Block builds already supply their descriptors.

Server dependency

Prod now serves the sign-in routes, since bbpci #4069 merged: app.builderlab.xyz/api/goose/v1/session returns 401 and /v1/login/start returns 400 on an empty request. Create account doesn't send screen_hint=signup yet; that comes in a follow-up.

@baxen
baxen requested review from a team as code owners November 5, 2026 03:49
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T03:55:06.686630Z 4e64d85 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/app/IdentitySettings.tsx Resolved
Comment on lines +1089 to +1090
for server in host.servers() {
snapshots.push(server.restore().await);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Browser fixture migration and the primary-server sign-in control need repair. The existing restore/sign-out recovery finding also remains unresolved.

Comment thread src/bundled/builderlab/oauth/session.ts Resolved
Comment thread src/bundled/builderlab/index.tsx Resolved
Comment thread src/app/IdentitySettings.tsx Resolved

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 One documentation contract mismatch in the header repair; existing findings remain unresolved.

Comment thread src-tauri/src/browser/builderlab.rs Resolved

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Additional full-review finding.

Comment thread src/bundled/builderlab/index.tsx Resolved

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Additional full-review finding.

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Additional full-review finding.

Comment thread vite.config.ts
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [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 kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Additional onboarding finding.

Comment thread src/features/identity/Welcome.tsx Resolved
baxen and others added 3 commits November 5, 2026 22:42
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>
@matt2e
matt2e force-pushed the mbot/one-login-main branch from ef3bafe to 42ccede Compare November 5, 2026 11:43

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 One documentation correction; previously reported runtime findings remain unresolved.

Comment thread docs/identity.md Resolved
matt2e and others added 2 commits November 5, 2026 23:20
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 kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Shared-session reconciliation still loses sign-out and account-switch intent. Two reproduced P2 defects and a statically traced renewal gap are detailed inline.

Comment thread src-tauri/src/federated_identity/mod.rs Resolved
Comment thread src-tauri/src/federated_identity/mod.rs Outdated
state.restored && state.session.is_none()
};
let attempt =
if signed_out && matches!(self.storage(|store| store.read()).await, Ok(Some(_))) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [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.

Comment thread src-tauri/src/federated_identity/mod.rs Resolved
matt2e and others added 9 commits November 6, 2026 00:20
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 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 requesting changes for four things. Thufir and I reviewed this at 15f984c289 from the source only, with no live run yet.

  1. 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 was unavailable, not signed-out: first-run.ts#L109-L117. so the user lands in account/key setup as the other copy's account without choosing it, which contradicts docs/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.

  2. "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 via login.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.

  3. operation() returns the latest pushed snapshot, not the invoke result: service.ts#L168-L179, copied into CommunitySession. native publishes signing-in mid-call, which bumps the epoch, so the invoke result is always dropped and the caller gets whatever event arrived last. if the final signed-in/signed-out event hasn't landed when the invoke resolves, Switch account's runSwitch reports "Couldn't switch your account" after a successful or canceled sign-in, and first-run.ts:123 throws. 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 only accept()ing it when the epoch is unchanged) removes the dependency. a test where a signing-in event arrives before the invoke resolves would pin it.

  4. key recovery still runs its own legacy browser login. "Restore your key" starts a PKCE /v1/auth/login?type=cli login (nsec_backup.rs#L680-L700) and uses the resulting Builderlab credential as X-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 as Authorization: Bearer like 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>
@wpfleger96

Copy link
Copy Markdown
Member

🤖 dropping item 4 (key recovery on the NIP-FI session) from my review. kgoose's recovery route (RecoverHpkeNsecBackupAction) deliberately requires its own CLI-type session in X-BB-Session-Credential, so the separate recovery login is a server requirement, not a client leftover. items 1-3 still stand.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Two first-run failure paths remain.

// browser chooses.
if (
before.status === "unavailable" &&
(before.status === "unavailable" || before.status === "signed-out") &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [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.

wpfleger96 and others added 2 commits November 5, 2026 12:34
…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 kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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>
@wpfleger96
wpfleger96 dismissed their stale review November 5, 2026 20:49

addressed

@wpfleger96
wpfleger96 merged commit 54cffc1 into main Nov 5, 2026
52 checks passed
@wpfleger96
wpfleger96 deleted the mbot/one-login-main branch November 5, 2026 21:07
baxen added a commit that referenced this pull request Nov 5, 2026
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>
wpfleger96 added a commit that referenced this pull request Nov 5, 2026
…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
wpfleger96 added a commit that referenced this pull request Nov 5, 2026
…-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
morgmart added a commit that referenced this pull request Nov 5, 2026
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>
baxen added a commit that referenced this pull request Nov 6, 2026
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>
morgmart added a commit that referenced this pull request Nov 6, 2026
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>
morgmart added a commit that referenced this pull request Nov 6, 2026
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>
morgmart added a commit that referenced this pull request Nov 6, 2026
…-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>
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.

4 participants