From 408ac348cce7fd9bca7b526a0990def018bacbee Mon Sep 17 00:00:00 2001
From: Bradley Axen
Date: Wed, 28 Oct 2026 15:41:18 -0700
Subject: [PATCH 1/3] feat(builderlab): one Builderlab settings page and
sign-in for communities and agents
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fold the Hosted communities plugin into the Builderlab plugin. Settings →
Plugins → Builderlab (Block logo) now holds the account, hosted communities
and remote agents. Desktop communities use the plugin's shared sign-in and the
configured BUZZ_BUILDERLAB_URL instead of a separate production login, and read
the account once sign-in's automatic identity link settles. Live development
with a pinned viewer keeps the broker. Create community links open the new page.
Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5
Signed-off-by: Bradley Axen
---
crates/plugin-manager/src/lib.rs | 5 -
docs/identity.md | 2 +-
docs/input-correction-audit.md | 2 +-
docs/plugin-architecture.md | 28 +-
docs/shell-design.md | 2 +-
docs/status.md | 2 +-
src-tauri/src/host_request.rs | 23 --
src/app/App.tsx | 2 +-
src/app/pages.integration.test.mjs | 16 +-
src/app/settings-sections.test.ts | 12 +-
src/bundled/builderlab/README.md | 15 +-
.../builderlab/agents/RemoteAgents.tsx | 48 ++--
.../communities}/HostedCommunities.test.tsx | 0
.../communities}/HostedCommunities.tsx | 150 +++++-----
.../communities}/api.test.ts | 0
.../communities}/api.ts | 0
.../communities}/native.test.ts | 269 ++++++++----------
.../communities}/native.ts | 209 +++++---------
src/bundled/builderlab/index.test.tsx | 45 ++-
src/bundled/builderlab/index.tsx | 64 ++++-
src/bundled/builderlab/manifest.json | 2 +-
src/bundled/hosted-communities/README.md | 9 -
src/bundled/hosted-communities/icon.png | Bin 58298 -> 0 bytes
src/bundled/hosted-communities/index.test.tsx | 186 ------------
src/bundled/hosted-communities/index.tsx | 24 --
src/bundled/hosted-communities/manifest.json | 10 -
src/bundled/index.ts | 7 -
src/features/communities/CommunityLanding.tsx | 2 +-
.../communities/CommunityPicker.test.tsx | 4 +-
src/features/communities/CommunityPicker.tsx | 4 +-
src/shared/design-system/icons/BlockLogo.tsx | 35 +++
src/shared/design-system/icons/index.ts | 8 +
32 files changed, 467 insertions(+), 718 deletions(-)
rename src/bundled/{hosted-communities => builderlab/communities}/HostedCommunities.test.tsx (100%)
rename src/bundled/{hosted-communities => builderlab/communities}/HostedCommunities.tsx (92%)
rename src/bundled/{hosted-communities => builderlab/communities}/api.test.ts (100%)
rename src/bundled/{hosted-communities => builderlab/communities}/api.ts (100%)
rename src/bundled/{hosted-communities => builderlab/communities}/native.test.ts (63%)
rename src/bundled/{hosted-communities => builderlab/communities}/native.ts (50%)
delete mode 100644 src/bundled/hosted-communities/README.md
delete mode 100644 src/bundled/hosted-communities/icon.png
delete mode 100644 src/bundled/hosted-communities/index.test.tsx
delete mode 100644 src/bundled/hosted-communities/index.tsx
delete mode 100644 src/bundled/hosted-communities/manifest.json
create mode 100644 src/shared/design-system/icons/BlockLogo.tsx
diff --git a/crates/plugin-manager/src/lib.rs b/crates/plugin-manager/src/lib.rs
index 9662d06499..4f8ce00daf 100644
--- a/crates/plugin-manager/src/lib.rs
+++ b/crates/plugin-manager/src/lib.rs
@@ -318,10 +318,6 @@ pub fn bundled_manifests() -> Vec {
.expect("workflows manifest"),
serde_json::from_str(include_str!("../../../src/bundled/feedback/manifest.json"))
.expect("feedback manifest"),
- serde_json::from_str(include_str!(
- "../../../src/bundled/hosted-communities/manifest.json"
- ))
- .expect("hosted communities manifest"),
serde_json::from_str(include_str!(
"../../../src/bundled/moderation/manifest.json"
))
@@ -364,7 +360,6 @@ fn enabled_by_default(id: &str) -> bool {
| "buzz.codex"
| "buzz.goose"
| "buzz.workflows"
- | "block.hosted-communities"
| "block.builderlab"
| "buzz.moderation"
| "buzz.relay-staff"
diff --git a/docs/identity.md b/docs/identity.md
index 3a3680f96a..1d262281b5 100644
--- a/docs/identity.md
+++ b/docs/identity.md
@@ -85,7 +85,7 @@ the signing key across rebuilds therefore extends trust to those programs.
Desktop builds configured with `BUZZ_NSEC_BACKUP_SERVICE_URL` and
`BUZZ_NSEC_BACKUP_NAMESPACE` back up a local identity only after an explicit saved
-automatic-backup choice, Builderlab or Hosted communities login, and matching key
+automatic-backup choice, Builderlab login, and matching key
binding. Unset, self-managed, corrupt or mismatched consent prevents all backup
requests, including retries. These are paired
native build-process inputs; unset leaves the feature disabled. The service URL
diff --git a/docs/input-correction-audit.md b/docs/input-correction-audit.md
index b45a0ca274..378897ae77 100644
--- a/docs/input-correction-audit.md
+++ b/docs/input-correction-audit.md
@@ -26,7 +26,7 @@ capitalization, but do not explicitly disable autocorrect.
| `features/communities/CommunityDialog.tsx` | Relay URL and invite code |
| `features/browser/BrowserHostView.tsx` | Address bar |
| `features/profiles/AvatarEditor.tsx` | Picture URL and emoji |
-| `bundled/hosted-communities/HostedCommunities.tsx` | Domain slug, transfer recipient npub |
+| `bundled/builderlab/communities/HostedCommunities.tsx` | Domain slug, transfer recipient npub |
| `bundled/channels/CreateChannelDialog.tsx` | Channel name |
| `bundled/channels/ChannelLifecycleDialog.tsx` | Exact channel-name confirmation |
| `bundled/workflows/WorkflowEditor.tsx` | Workflow identifiers and YAML |
diff --git a/docs/plugin-architecture.md b/docs/plugin-architecture.md
index 099924e12e..e61c2c72d1 100644
--- a/docs/plugin-architecture.md
+++ b/docs/plugin-architecture.md
@@ -509,7 +509,7 @@ contracts with their own layout and local navigation.
**Channels is the only required bundled plugin.** Bestie,
Todos and Usage are off by default. Feedback, Diff viewer, Identity
Naming, Agent Activity, Terminal, Links, GitHub, Inbox,
-Repositories, Agents, Claude, Codex, Workflows, Builderlab, Hosted communities,
+Repositories, Agents, Claude, Codex, Workflows, Builderlab,
Membership, and Moderation are on by default, but optional. Both browser and
native catalogs declare that policy.
@@ -523,7 +523,7 @@ without a saved flag use the current default, so changing a default also changes
those existing profiles. There is no migration or forced reset: a browser profile that previously saved its full
plugin snapshot can retain Bestie enabled. Native profiles store per-plugin
overrides. Default-on does not promise platform support: Terminal contributes UI
-only on macOS/Linux desktop; Hosted communities needs either the desktop app or
+only on macOS/Linux desktop; Builderlab's hosted communities need either the desktop app or
the live development broker. Disabling Membership removes its Invite to community shortcut.
Membership and Moderation share app-owned access controllers composed in
@@ -593,9 +593,10 @@ Create resolves its agents and its teams' members to those that are the owner's
agents in this community and sends them with the Canvas as ordinary channel setup,
which the session applies and retries as before.
-Hosted communities (`block.hosted-communities`) is a Block-specific bundled plugin
-under Settings → Plugins → Builderlab hosting. It manages Block-hosted relays through a Builderlab
-account: browser sign-in, binding the local Buzz identity (a locally signed kind
+Hosted communities are part of the Block-specific Builderlab plugin
+(`block.builderlab`, `communities/`), shown under Settings → Plugins → Builderlab
+beside remote agents. They manage Block-hosted relays through the plugin's one
+Builderlab sign-in: binding the local Buzz identity (a locally signed kind
24243 challenge), and create/archive/unarchive/transfer. A server-declared,
default-off capability also exposes owner deletion for archived communities. The
card persists the bound four-field request before admission. A fresh request can
@@ -610,14 +611,15 @@ re-read and verified before dispatch; browser local storage has no atomic compar
so exactly simultaneous contexts remain a documented client-side race;
it never signs deletion or infers acceptance from a missing list row. Joining
stays in the existing Join a community dialog; the card only copies the new relay address. Its
-account backend follows the same split as `HostService`: live development uses the
-broker's `/api/builderlab/*` routes (`browser-host/builderlab.mjs`), which keep the session
-credential and signing key in Node. Desktop builds use `native.ts`, the same routes
-over the shared `oauth_callback` sign-in, host HTTP to the manifest's single
-`https://app.builderlab.xyz` grant, and `identity_sign_builderlab_binding`, which
-validates the challenge and signs only the fixed kind 24243 binding. The desktop
-credential lives in plugin memory for the plugin's lifetime and is never persisted,
-so restarting the app signs out. Other browser builds show the unavailable notice.
+account backend follows the same split as `HostService`: live development with a
+pinned viewer uses the broker's `/api/builderlab/*` routes (`browser-host/builderlab.mjs`),
+which keep their own session credential and signing key in Node. Desktop builds use
+`native.ts`, the same routes with the plugin's shared session credential, host HTTP to
+the configured `BUZZ_BUILDERLAB_URL` grant, and `identity_sign_builderlab_binding`, which
+validates the challenge and signs only the fixed kind 24243 binding. They read the
+account after sign-in's automatic identity link settles. The desktop credential lives in
+plugin memory for the plugin's lifetime and is never persisted, so restarting the app
+signs out. Builds without a configured Builderlab URL show the unavailable notice.
### Composer accessories
diff --git a/docs/shell-design.md b/docs/shell-design.md
index 0412782c17..48f47b03f6 100644
--- a/docs/shell-design.md
+++ b/docs/shell-design.md
@@ -65,7 +65,7 @@ semantic tokens, UI authoring rules and the local component reference.
a shared Popover containing NavigationItem rows with their existing context menus;
this richer picker retains community actions as well as switching. Arrow keys,
Home and End move focus. Join and Create community appear at the bottom; creation
- opens the registered Hosted communities settings flow. Dialogs return focus to
+ opens the registered Builderlab settings flow. Dialogs return focus to
the surviving community trigger.
Switching pages preserves the desktop sidebar’s current visibility. The manual
collapse toggle remains available across destinations.
diff --git a/docs/status.md b/docs/status.md
index f60eed138c..fc3fd134bd 100644
--- a/docs/status.md
+++ b/docs/status.md
@@ -55,7 +55,7 @@ and [contributing](contributing.md) for maintained commands and test organizatio
| Gate | What is still needed |
| --- | --- |
| Native lifecycle acceptance | Agree app-data **and keyring** isolation, then exercise actual CLI install/update → running-app observation, on-disk rollback/backup, disable/recovery/safe mode and process restart. Browser fixture storage is not native IPC/filesystem evidence. |
-| Native identity and networking | External plugins can declare exact commands and HTTPS origins for bounded native calls. Packaged real-data acceptance and per-user login/session behavior remain open. Hosted communities' Builderlab sign-in and identity binding run natively in desktop builds; the session lasts until the app restarts. The declarations are disclosure, not plugin isolation; private keys stay outside plugin JavaScript. Do not widen CSP to unrestricted networking. |
+| Native identity and networking | External plugins can declare exact commands and HTTPS origins for bounded native calls. Packaged real-data acceptance and per-user login/session behavior remain open. The Builderlab sign-in, shared by hosted communities, remote agents and apps, and its identity binding run natively in desktop builds; the session lasts until the app restarts. The declarations are disclosure, not plugin isolation; private keys stay outside plugin JavaScript. Do not widen CSP to unrestricted networking. |
| Supported author contract | The conversation preview adds generated type-only `@buzz/author`, scaffold consumption and external Composer/Message reuse. Final validation, archived cross-host compatibility and native acceptance remain open; no stable SDK promise. |
| Independent plugin acceptance | Independently build a real-data page and a non-GitHub panel without private host imports, copied contracts or bespoke host patches. In a packaged app: install disabled → enable/use → A/B switching → update/remount → disable → rollback → failed-start recovery. Include revocation, unknown/rejected writes and stalled cleanup. |
| Distribution | PR CI is wired in `.github/workflows/ci.yml`; decide release signing, updating, provenance and compatibility support. Record packaged artifacts and platform-specific results. Native test targets at the recorded gate contain zero cases; compilation is not GUI acceptance. |
diff --git a/src-tauri/src/host_request.rs b/src-tauri/src/host_request.rs
index c7453b400e..0968c96f1f 100644
--- a/src-tauri/src/host_request.rs
+++ b/src-tauri/src/host_request.rs
@@ -499,29 +499,6 @@ mod tests {
assert!(!allows_origin(&grants, &url));
}
- #[test]
- fn bundled_hosted_communities_reach_only_the_builderlab_origin() {
- let temp = tempfile::tempdir().unwrap();
- let manager =
- buzzodz_plugins::Manager::open(Some(temp.path().into()), "test", false).unwrap();
- let grants = manager
- .host_grants("block.hosted-communities", "bundled")
- .unwrap();
- assert_eq!(grants.network_origins, ["https://app.builderlab.xyz"]);
- assert!(grants.commands.is_empty());
- for (url, allowed) in [
- ("https://app.builderlab.xyz/api/goose/v1/auth/me", true),
- ("https://builderlab.xyz/api/goose/v1/auth/me", false),
- (
- "https://app.builderlab.xyz:8443/api/goose/v1/auth/me",
- false,
- ),
- ] {
- let (url, _, _) = validate_request(&request(url)).unwrap();
- assert_eq!(allows_origin(&grants, &url), allowed, "{url}");
- }
- }
-
#[test]
fn uploads_only_an_archive_file_in_home() {
let url = reqwest::Url::parse("https://apps.example.test/deploy").unwrap();
diff --git a/src/app/App.tsx b/src/app/App.tsx
index 9d98db9a67..008c16734c 100644
--- a/src/app/App.tsx
+++ b/src/app/App.tsx
@@ -299,7 +299,7 @@ function ConnectedApp({ services }: { services: AppServices }) {
void services.navigation.open({
version: 1,
kind: "settings",
- section: "block.hosted-communities/hosted",
+ section: "block.builderlab/login",
})
}
/>
diff --git a/src/app/pages.integration.test.mjs b/src/app/pages.integration.test.mjs
index 5070072d89..8431eede7f 100644
--- a/src/app/pages.integration.test.mjs
+++ b/src/app/pages.integration.test.mjs
@@ -135,13 +135,7 @@ test("the app runtime exposes ready bundled pages and removes them on disable",
.snapshot()
.map((card) => card.pluginId)
.sort(),
- [
- "block.builderlab",
- "block.hosted-communities",
- "buzz.channels",
- "buzz.emoji",
- "buzz.pairing",
- ],
+ ["block.builderlab", "buzz.channels", "buzz.emoji", "buzz.pairing"],
);
// The device-local template library remains reachable without a selected community.
assert.equal(
@@ -207,13 +201,7 @@ test("the app runtime exposes ready bundled pages and removes them on disable",
.snapshot()
.map((card) => card.pluginId)
.sort(),
- [
- "block.builderlab",
- "block.hosted-communities",
- "buzz.channels",
- "buzz.emoji",
- "buzz.pairing",
- ],
+ ["block.builderlab", "buzz.channels", "buzz.emoji", "buzz.pairing"],
);
await vi.waitFor(() =>
diff --git a/src/app/settings-sections.test.ts b/src/app/settings-sections.test.ts
index 367424177c..d88a38fae4 100644
--- a/src/app/settings-sections.test.ts
+++ b/src/app/settings-sections.test.ts
@@ -32,13 +32,9 @@ const cards = [
card("other/groups"),
card("example/membership", { section: "administration" }),
card("example/hosted", { group: "Communities" }),
- card("block.hosted-communities/hosted", {
- group: "Plugins",
- title: "Builderlab hosting",
- }),
card("block.builderlab/login", {
group: "Plugins",
- title: "Builderlab agents",
+ title: "Builderlab",
}),
];
const primary = { id: "primary", name: "Primary" };
@@ -53,11 +49,7 @@ const app = [
"updates",
...(developerMode ? ["developer"] : []),
];
-const plugins = [
- "plugins",
- "block.builderlab/login",
- "block.hosted-communities/hosted",
-];
+const plugins = ["plugins", "block.builderlab/login"];
const outline = (model: ReturnType) =>
model.groups.map((group) => [
group.id,
diff --git a/src/bundled/builderlab/README.md b/src/bundled/builderlab/README.md
index b42cf5598f..ca3e04ea2d 100644
--- a/src/bundled/builderlab/README.md
+++ b/src/bundled/builderlab/README.md
@@ -1,13 +1,14 @@
# Builderlab
-Create agents that Builderlab hosts for you, so they keep working even when Buzz is closed.
+One Builderlab account for communities and agents that Builderlab hosts for you, so they keep working even when Buzz is closed.
## Getting started
-1. Go to **Settings → Plugins → Builderlab agents** and choose **Sign in with Builderlab**.
-2. In **Agents**, choose **New agent** and pick **Builderlab (hosted)** as the **Harness**.
-3. Choose a model and reasoning effort, optionally add instructions, then create the agent.
-4. Edit instructions later in **Identity**, and model settings in **Settings**.
+1. Go to **Settings → Plugins → Builderlab** and choose **Sign in with Builderlab**.
+2. To host a community, choose **Create community** under **Communities**, then **Copy address**, open the community picker and choose **Join a community**.
+3. To host an agent, in **Agents** choose **New agent** and pick **Builderlab (hosted)** as the **Harness**.
+4. Choose a model and reasoning effort, optionally add instructions, then create the agent.
+5. Edit instructions later in **Identity**, and model settings in **Settings**.
---
@@ -43,7 +44,9 @@ SSO is a separate integration.
## Remote agents
-The bundled `block.builderlab` plugin adds Settings → Plugins → Builderlab agents.
+The bundled `block.builderlab` plugin adds Settings → Plugins → Builderlab: one
+sign-in, then hosted communities (`communities/`, see
+[plugin architecture](../../../docs/plugin-architecture.md)) and remote agents.
With a valid `BUZZ_BUILDERLAB_URL` build input, it also registers
`block.builderlab/builderlab` in Agents' new-agent type picker.
`adapter-type/` connects this hosted type to Beekeeper: provision registers the
diff --git a/src/bundled/builderlab/agents/RemoteAgents.tsx b/src/bundled/builderlab/agents/RemoteAgents.tsx
index f33526815a..a1205a865a 100644
--- a/src/bundled/builderlab/agents/RemoteAgents.tsx
+++ b/src/bundled/builderlab/agents/RemoteAgents.tsx
@@ -4,10 +4,14 @@ import {
useState,
useSyncExternalStore,
} from "react";
-import { RobotIcon } from "../../../shared/design-system/icons";
+import {
+ ArrowsClockwiseIcon,
+ RobotIcon,
+} from "../../../shared/design-system/icons";
import { Button } from "../../../shared/design-system/ui/Button";
import { EmptyState } from "../../../shared/design-system/ui/EmptyState";
import { InlineHeader } from "../../../shared/design-system/ui/Header";
+import { SettingsGroup } from "../../../shared/design-system/ui/SettingsGroup";
import type { OAuthSession } from "../oauth/session";
import type { AgentClient, RemoteAgent } from "./client";
@@ -82,12 +86,18 @@ function AgentList({
return (
+ {" "}
+ {error ? "Retry" : "Refresh agents"}
+
+ }
/>
{loading && (
);
}
diff --git a/src/bundled/hosted-communities/HostedCommunities.test.tsx b/src/bundled/builderlab/communities/HostedCommunities.test.tsx
similarity index 100%
rename from src/bundled/hosted-communities/HostedCommunities.test.tsx
rename to src/bundled/builderlab/communities/HostedCommunities.test.tsx
diff --git a/src/bundled/hosted-communities/HostedCommunities.tsx b/src/bundled/builderlab/communities/HostedCommunities.tsx
similarity index 92%
rename from src/bundled/hosted-communities/HostedCommunities.tsx
rename to src/bundled/builderlab/communities/HostedCommunities.tsx
index e159fbc96f..8207fd8e6f 100644
--- a/src/bundled/hosted-communities/HostedCommunities.tsx
+++ b/src/bundled/builderlab/communities/HostedCommunities.tsx
@@ -1,4 +1,4 @@
-import { SettingsGroup } from "../../shared/design-system/ui/SettingsGroup";
+import { SettingsGroup } from "../../../shared/design-system/ui/SettingsGroup";
import {
type ReactNode,
useCallback,
@@ -9,14 +9,14 @@ import {
useState,
} from "react";
import { npubEncode } from "nostr-tools/nip19";
-import { AlertDialog } from "../../shared/design-system/ui/AlertDialog";
-import { Button } from "../../shared/design-system/ui/Button";
-import { Checkbox } from "../../shared/design-system/ui/Checkbox";
-import { Dialog } from "../../shared/design-system/ui/Dialog";
-import { EmptyState } from "../../shared/design-system/ui/EmptyState";
-import { Header, InlineHeader } from "../../shared/design-system/ui/Header";
-import { Field } from "../../shared/design-system/ui/Field";
-import { Input } from "../../shared/design-system/ui/Input";
+import { AlertDialog } from "../../../shared/design-system/ui/AlertDialog";
+import { Button } from "../../../shared/design-system/ui/Button";
+import { Checkbox } from "../../../shared/design-system/ui/Checkbox";
+import { Dialog } from "../../../shared/design-system/ui/Dialog";
+import { EmptyState } from "../../../shared/design-system/ui/EmptyState";
+import { InlineHeader } from "../../../shared/design-system/ui/Header";
+import { Field } from "../../../shared/design-system/ui/Field";
+import { Input } from "../../../shared/design-system/ui/Input";
import {
ArchiveIcon,
ArrowSquareOutIcon,
@@ -30,7 +30,7 @@ import {
SignOutIcon,
TrashIcon,
WarningCircleIcon,
-} from "../../shared/design-system/icons";
+} from "../../../shared/design-system/icons";
import {
ApiFailure,
boundKey,
@@ -69,9 +69,12 @@ type Confirm = {
export function HostedCommunities({
api,
active,
+ sharedSignIn = false,
}: {
api: Api;
active(): boolean;
+ /** The surrounding page owns sign-in, sign-out and the account row. */
+ sharedSignIn?: boolean;
}) {
const activeRef = useRef(active);
activeRef.current = active;
@@ -417,11 +420,14 @@ export function HostedCommunities({
};
return (
-
-
+
{failure}
{unsupported ? (
@@ -441,67 +447,71 @@ export function HostedCommunities({
Checking sign-in…
) : !auth ? (
- }
- title="Sign in to manage hosted communities"
- description="Sign in through your browser to manage Block hosting. The rest of Buzz works without a Builderlab account."
- action={
- <>
-
- {action === "login" && (
-