Skip to content

feat(plugins): fetch a packaged managed plugin policy from a URL - #2504

Merged
joahg merged 3 commits into
block:mainfrom
joahg:joah/packaged-managed-plugins
Nov 11, 2026
Merged

joahg merged 3 commits into
block:mainfrom
joahg:joah/packaged-managed-plugins

Conversation

@joahg

@joahg joahg commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Summary

This adds BUZZ_BUILD_MANAGED_PLUGINS_URL, a native build input. A packager uses it to point Buzz at a managed plugin policy that it publishes. Every install then recommends the same plugins without a machine manager writing managed-plugins.json, and the pins stay current as the packager re-releases.

The first version of this PR compiled the policy itself into the build. That goes stale the first time the packager re-releases a plugin. Git imports require the reference to point at the pinned commit, so a fresh install gets refused, and anyone who updated sees a lasting Outdated badge. This version fetches the policy instead.

  • Fetching: the desktop app (src-tauri/src/packaged_plugins.rs) fetches the URL at startup and every 6 hours, retrying after 15 minutes when an attempt fails.
    • It refuses non-https URLs and redirects, times out after 30 seconds, and caps the body at 256 KiB.
  • Validating: managed::store_packaged keeps a response only if it parses as a usable version 1 policy. It writes the copy atomically to packaged-managed-plugins-<hash of the URL>.json beside the profile folders, so a copy is only ever shown for the URL it came from. A usable policy from a new URL deletes copies kept for other URLs. A failed or unusable fetch leaves the last good copy in place.
  • Before the first fetch: with no stored copy, the managed status is loading, or unavailable with the reason after a failure. Settings → Plugins shows either state with a Retry button (plugin_managed_refresh, registered in build.rs and the default capability), and opening Settings after a failure retries once.
  • Precedence: managed::effective uses the stored copy only when no managed-plugins.json exists and BUZZ_MANAGED_PLUGINS doesn't name a file. A machine manager's file still replaces it completely.
  • Build check: build.rs accepts only an https URL with no credentials or fragment, at most 2048 characters. Leave the variable unset for OSS builds.

It still only recommends. Every install goes through the existing Git or URL import (#2910), with its access review and its commit or SHA-256 pin.

Remote policy fetching is new for Buzz, so a design read on that part is especially welcome.

Test plan

  • cargo test -p buzzodz-plugins passes (88, 19 and 31 tests). New tests:
    • reports_a_packaged_policy_that_has_not_arrived
    • keeps_each_packaged_copy_for_its_own_url
    • falls_back_to_the_stored_packaged_policy_only_without_a_file
    • stores_only_usable_packaged_policies
    • packaged_managed_plugins_url_accepts_only_plain_https
  • vitest for src/app/ManagedPlugins.test.tsx and src/plugins passes (1323 across src/app and src/plugins). The new cases cover loading, a first-fetch failure with Retry and retry-on-open followed by recovery, and no retry while busy. tsc and biome are clean.
  • cargo test -p Buzz --lib passes except converts_tiled_heic_without_cropping, which depends on the local ffprobe output and touches nothing in this PR. The huddle stalled-write test failed once under load and passes when run alone. The permission tests pass with plugin_managed_refresh.
  • cargo fmt --check passes. cargo clippy --locked --all-targets -D warnings is clean for buzzodz-plugins and the desktop crate.
  • Native check against a copy of a real desktop profile, from a scratch crate built with BUZZ_BUILD_MANAGED_PLUGINS_URL set to a live https policy URL:
    • Before storing, Manager::managed is absent.
    • store_packaged_policy returns true, and the policy turns ready with its source and its one entry.
    • Storing the same bytes again returns false.
    • A version: 2 body is refused, and the last good copy stays.
    • Writing a managed-plugins.json replaces the packaged copy.
    • With no copy yet, the status is loading, then unavailable with the reason after a recorded failure.
  • Earlier, a temporary live test, not committed, ran fetch against the same URL. An http:// URL was refused.

🤖 Generated with Claude Code

@joahg joahg changed the title feat(plugins): packaged managed plugin policy feat(plugins): fetch a packaged managed plugin policy from a URL Nov 5, 2026
joahg added a commit that referenced this pull request Nov 7, 2026
…HA-256, including requests and links (#2910)

## Why

About 30–40% of people at Block can't clone the Git host where a plugin
lives. Managed plugin recommendations, and Block's default marketplace
plugin, only install through a Git import today (`InstallSource` is
`Folder | Git`), so those people see a recommendation they can't
install. AGNTOPS-753, a follow-up to AGNTOPS-689.

## What

A third install source: one single-file plugin artifact, downloaded over
HTTPS and pinned by SHA-256.

- **`InstallSource::Url { url, sha256 }`.** `imports::prepare_url`
accepts the bytes only when they hash to the pin. They then go through
the same artifact validation and import preview as a Git or folder
import. The installed revision is that SHA-256.
- **`plugin_import_url`** (Tauri) downloads at most 8 MiB. It uses https
only (redirects too), with a 60 s timeout, then hands the bytes to
`prepare_url`. Like a Git clone, it holds the import slot for the whole
download, so a review the person closed can't finish later and replace a
newer import's preview. Nothing is written until the person confirms the
review.
- **Managed policy.** A `managed-plugins.json` entry may be `{ "id",
"url", "sha256" }` instead of the Git fields. Such an entry is
**Current** when the installed revision equals the pin, from any source.
It's **Outdated** when the plugin was installed from a URL at another
revision, and **Other** otherwise. The Settings review downloads and
previews it with no Git call.
- **`buzzodz plugin pack DIST FILE`** writes the artifact, the same
bytes as `plugin.artifact.json`, unsigned. It prints `{id, file,
sha256}` so a release pipeline can publish it and write the pin.
- **Install requests and links.** `pluginInstaller.requestInstall` and
`buzz://plugin-install` also accept `{ id, url, sha256 }`. The
marketplace plugin uses these to install other plugins. A URL request or
link opens the same review as a recommended plugin: Buzz downloads the
artifact, checks the hash and plugin ID, and installs only after the
person confirms. It can update a plugin that was installed from a URL.
It refuses a plugin installed from Git or a folder, and names both
sources in the message. The Git import dialog ignores URL requests. A
link can't mix the URL keys with the Git keys. `pluginInstaller.sources`
(`["git", "url"]`) lets a plugin check that the host accepts URL sources
before it asks for one; squareup/agents#9412 uses it in the marketplace.
- The docs (`managed-plugins.md#url-entries`, `plugin-architecture.md`,
`deep-links.md`) cover the schema, the link, `requestInstall` and the
pack command.

URL installs carry no release signature, and Git update checks skip
them: a URL is one immutable version, and updates arrive through a new
recommendation.

## Not in this PR

- squareup/agents publishing the packed artifacts to Artifactory and
writing `url`/`sha256` into the default policy and catalog.

## Validation

- `cargo test -p buzzodz-plugins`: all pass, including new tests for:
  - the hash mismatch and unsafe URLs
  - install, then reopen, keeping the URL source
  - URL policy parsing and the Current/Outdated/Other states
- `cargo clippy` (plugin-manager and src-tauri) is clean. src-tauri
`browser_permissions` passes with the new command allowed for the main
webview.
- `vitest`: new tests cover:
- a URL recommendation that reviews and installs without Git, and
refusal of an artifact with the wrong plugin ID
- a URL install request that resolves `installed`, `cancelled` on close,
and is refused over a Git-installed plugin
- Settings opening a URL request in the review instead of the add dialog
  - `requestInstall` URL source parsing
  - valid and ambiguous `buzz://plugin-install` URL links
- Full `vitest` run: everything passes except two tests. The HEIC test
(`media-preparation`) also fails on the base commit.
`MemberAdministration` passes when rerun alone.
- `buzzodz plugin pack` on a sample dist: the printed SHA-256 matches
`shasum -a 256` of the output.
- Design review fixes (053b926): the URL review title names the
plugin. A URL-installed plugin without a recommendation shows its source
URL and SHA-256 in details, plus guidance to update from the marketplace
or a new link, instead of a folder or Git update. Test fixtures use
example.org.
- Browser tests (`tests/browser/plugin-import.spec.mjs`, native IPC
fixture) cover three flows: a recommended URL that installs the pinned
artifact without any Git call, a download whose SHA-256 doesn't match
that never installs, and a `buzz://plugin-install?id=&url=&sha256=` link
that reviews and installs once. The link test caught a bug that the
second commit fixes: the URL link review and the Git import dialog
shared a React key, so the review kept remounting and downloading again.
That test fails without the fix. The plugin browser suites
(`plugin-import`, `plugin-defaults`, `plugins`, `buzz-links`) pass on
Chromium and WebKit (72 tests), and `vitest` over `src/app`,
`src/plugins` and `src/features/navigation` passes (1,552 tests).
- Native path, run against a copy of a real desktop plugin profile:
`managed::status` showed a Git-installed md-reader as Current for its
artifact URL, because the Git revision equals the artifact's SHA-256.
`prepare_url` and install of the squareup/agents#9412-packed Math
artifact gave an enabled plugin with a URL update source, which showed
as Current and stayed that way after reopening. A wrong SHA-256 and an
`http://` URL were refused.
- Review fix (ba2b041, kazemek21's P2):
`a_url_download_holds_the_import_slot_until_it_settles` gates a URL
download and checks that a second import is refused while it is pending
and that the slot frees afterwards. It fails against the old
download-then-lock ordering.
- Anonymous HTTPS reads from
`global.block-artifacts.com/.../support-assets/buzz-plugins/` return
200. The real artifacts appear once squareup/agents#9412 merges.

@tellaho, this is the non-Git source we talked about for the managed
recommendations in #2504. Could you do a design read?

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Joah Gerstenberg <[email protected]>
Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>
@joahg
joahg force-pushed the joah/packaged-managed-plugins branch from 235f7f3 to 3d2d364 Compare November 9, 2026 21:34
@joahg
joahg marked this pull request as ready for review November 9, 2026 21:35
@joahg
joahg requested a review from a team as a code owner November 9, 2026 21:35
@joahg
joahg requested review from kazemek21 and tellaho November 9, 2026 21:35

@tellaho tellaho 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.

🤖 Adversarial review by Princess Donut, on tho's behalf. One UX correctness finding below: first-fetch failure is invisible to the person trying to discover the recommended plugins. Source review only; native interaction testing remains pending.

Comment thread src-tauri/src/packaged_plugins.rs Outdated
let wait = match refresh(&client, &manager).await {
Ok(()) => REFRESH,
Err(error) => {
eprintln!("Could not refresh the packaged plugin policy: {error}");

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] Surface policy-fetch failure in plugin settings. On a fresh installation with no cached policy, launching before the network/VPN is ready (or receiving an HTTP error or invalid policy) reaches this branch and only writes stderr. managed::effective then returns Absent, pendingManaged returns zero, and Settings renders no recommendation or error. Restoring connectivity or reopening Settings does not trigger this fetch; the user waits 15 minutes or restarts the app, with no indication why the recommended marketplace is missing. Please expose fetch/loading/error state alongside the cached policy and provide a retry action, retaining the last good recommendations on failure. Cover first-fetch failure followed by recovery so an unavailable configured policy is distinguishable from a build with no recommendations.

@joahg joahg Nov 9, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 Fixed in 5d65f3b.

  • Manager: it now records each policy fetch's outcome (pending, fetched, or failed with a reason). When the build names a packaged URL and no copy exists, managed::effective returns {status: "loading", url} or {status: "unavailable", url, reason} instead of Absent. Once a copy exists, it still wins, so a failed refresh keeps the last good recommendations. A managed-plugins.json still overrides everything.
  • Retry: a new plugin_managed_refresh command fetches immediately, serialized with the background loop, then returns the catalog. PluginManager.refreshManaged() runs it with a 45s watchdog.
  • Settings: shows "Loading the recommended plugin list…", or "Recommended plugins unavailable" with the URL, the reason and a Retry button. Opening Settings after a failure retries once. A successful fetch in the background appears on Settings' next read of the plugin list, which happens once a second.
  • Tests:
    • reports_a_packaged_policy_that_has_not_arrived covers loading, unavailable, keeping the last good copy after a failure, and the file override.
    • ManagedPlugins.test.tsx covers the loading state; a first-fetch failure that shows the reason, retries on open and on Retry, then recovers to "Recommended by …"; and no retry while busy.
    • Natively, against a copy of a real profile with a live policy URL compiled in: loading, then unavailable after a recorded failure, then ready after storing the live policy.

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.

🤖 [P1] Allow the new policy refresh command through Tauri

The Retry button and retry-on-open invoke plugin_managed_refresh, but it is missing from the app manifest’s command list and the capability permissions. The native permission test now fails with plugin_managed_refresh is not in build.rs, so desktop Retry cannot invoke the handler. Please add both registrations so the recovery path works.

Posted by Codex (gpt-6.1-sol) on behalf of kylez

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 Fixed in 8c279d6. plugin_managed_refresh is now registered in src-tauri/build.rs, capabilities/default.json (allow-plugin-managed-refresh) and the browser permission test list. cargo test -p Buzz --lib permission passes. I also ran the full desktop suite this time: the only failures are converts_tiled_heic_without_cropping (local ffprobe output) and the huddle stalled-write test, which passes when run alone. Neither touches this change.

@joahg
joahg requested a review from tellaho November 9, 2026 21:48
Comment thread crates/plugin-manager/src/managed.rs Outdated
let explicit = env.as_ref().is_some_and(|value| !value.is_empty());
match (status(path(home, env), catalog), packaged) {
(Managed::Absent, Some((url, fetch))) if !explicit => {
match (status(Ok(home.join(PACKAGED_FILE_NAME)), catalog), fetch) {

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] Bind cached recommendations to the configured URL

This reads the same cached policy for every packaged URL. If an existing installation changes from URL A to B and B’s download fails, it keeps recommending A’s plugins. Please associate the cache with its URL and retain last-good recommendations only for that same source.

Posted by Codex (gpt-6.1-sol) on behalf of kylez

@joahg joahg Nov 9, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 Fixed in 8c279d6. The last good copy is now packaged-managed-plugins-<first 16 hex chars of sha256(url)>.json, and effective reads only the copy for the configured URL. So after switching from A to B, a failed fetch of B shows Recommended plugins unavailable, not A's plugins. The first usable policy from B deletes the copies kept for other URLs, including the earlier unkeyed packaged-managed-plugins.json. keeps_each_packaged_copy_for_its_own_url covers both, and the docs say so.

kazemek21 added a commit that referenced this pull request Nov 9, 2026
… an update (#3024)

## TL;DR

A `buzz://plugin-install` Git link pinned to both a `commit` and a
`path`, for a plugin already installed from that same repository and
folder, now opens the **Update** review that a plugin's Git
`requestInstall` already gets. If the link names another branch or tag
than the plugin tracks, the review says so. Before, it hit the Add
path's **Already installed** refusal, so there was no reviewed way to
update from such a link. Links without both pins, and links to a plugin
installed from another source, are still refused as duplicates.

## What changed

- When the installed plugin was imported (not bundled), `PluginImport`
treats a link with both `commit` and `path` like a bound
`requestInstall` request when deciding between update and duplicate
(`src/app/PluginImport.tsx`). It reuses the existing same-source check:
the installed `updateSource` must be a Git source whose repository
matches the fetched preview's source and whose folder matches the
candidate's folder, and the candidate must have the installed manifest
ID. Native import saves the requested `ref`, and later update checks
follow it. So when a link's or plugin request's `ref` differs from the
installed Git source's ref, the review adds "Future updates will follow
<new> instead of <old>." An empty ref reads as "the repository default".
- Confirming a requested update (link or plugin request) re-reads the
live catalog and refuses with "The installed plugin changed. Close this
review and check again." if the installed revision, enabled state or
saved source changed after the review opened, so the review's "stays
disabled" line can't go stale.
- A requested update of a disabled plugin now says "This plugin will
stay disabled.", as the Details **Update** review already does. Native
install already keeps the enabled state, and the review offers no enable
switch for updates.
- Docs: `docs/plugin-architecture.md`, `docs/deep-links.md` and
`docs/buzz-help.md` describe the pinned-link update.

Protections that still apply: the commit pin (native refuses a ref whose
tip differs, and the review re-checks the fetched commit), the folder
filter, the access review against the installed manifest, the publisher
line and native publisher-continuity refusal, the unsigned-to-signed
rollback warning, explicit **Update** confirmation, and preview discard
on cancel. Links with only `commit`, only `path`, or neither, and pinned
links to a bundled plugin, keep showing **Already installed** / **Open
in Buzz**.

Not in scope: update discovery, Update-all, managed policy feeds or
checkers (#2504, #2621, #637), signature or publisher-trust policy
(#2212, #2746), and the compact request dialog (#2625). This PR doesn't
touch their hunks. #2983 and #2762 also edit
`tests/browser/plugin-import.spec.mjs`; this PR leaves that file
unchanged.

## Supported operation

On a Buzz desktop build that includes this change, open
`buzz://plugin-install?repository=<repo>&ref=<ref>&path=<folder>&commit=<40-or-64-hex>`
for a plugin whose installed copy came from a Git import of that same
repository and folder. The review shows **Update <name>**, with access
changes listed against the installed version, and states any change to
the branch or tag later updates follow. Nothing changes until you choose
**Update**. Native import still refuses the update if the ref's tip
isn't the pinned commit, or if the publisher changed or a signed plugin
became unsigned.

## Tests

New `src/app/PluginImport.test.tsx` cases, all with fictional
`example.com` fixtures:

- A pinned link to the installed source reviews as **Update**, installs
the pinned folder, and doesn't call enable. This is the regression test
for the original failure.
- A disabled plugin stays disabled and the review says so.
- A pinned link with another ref than the installed plugin tracks (a
named ref, or the repository default) reviews as **Update**, says which
ref future updates will follow, and installs on **Update**. A same-ref
link shows no such line.
- A link without both pins (none, path only, commit only), or a pinned
link to a bundled plugin, still shows **Already installed** with **Open
in Buzz**.
- An installed source in another repository or folder, a local folder,
or an unknown source is refused with the source-mismatch message and
**Open in Buzz**.
- When only another plugin ID is installed from that source, the link
offers a new install.
- A fetched commit that differs from the pin offers no **Update**.
- Cancel discards the preview without installing.
- If the installed revision, enabled state or source changes after the
review opens, **Update** is refused, for both a link and a plugin's
`requestInstall`.

The request and link harnesses now return a ready catalog from
`plugins.snapshot()`, which the new confirm-time check reads.

Checked on a Linux box at `37cf73e1`: `biome check` on both files, `pnpm
typecheck`, and `vitest run src/app/PluginImport.test.tsx` (100 passed).
After the first review fixes, the same three checks passed locally (102
tests), and the bundled-plugin test fails on the earlier predicate. The
later enabled-state drift case (two lines plus one test input) passed
biome locally; host load refused a local test run, so PR CI covers it.
With only the `requested` line reverted, 6 of the 8 `pinned link` tests
fail. Against `main`'s `PluginImport.tsx`, the same 6 fail and the other
94 pass. The two that pass either way are the existing pin-retry test
and the other-plugin-ID new-install case, which should behave the same
before and after.

After review feedback, the tracked-ref disclosure was verified on a Blox
box at `e913727f`: `biome check`, `pnpm typecheck` and `vitest run
src/app/PluginImport.test.tsx` (103 passed). With the review line
disabled, the new tracked-ref test fails. The before/after screenshots
predate this line and show a same-ref update.

### Before / after

These screenshots come from a temporary Chromium journey using the
existing `nativeImports` IPC fixture and fictional `example` data; the
journey is not committed. It installs `example.linked` from
`https://github.com/example/plugins` at `plugins/linked`, then delivers
the same pinned link again with a newer candidate.

| | Light | Dark |
|---|---|---|
| Before (`main`) |
![t027-before-light](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/3024/t027-before-light.png)
|
![t027-before-dark](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/3024/t027-before-dark.png)
|
| After |
![t027-after-light](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/3024/t027-after-light.png)
|
![t027-after-dark](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/3024/t027-after-dark.png)
|

## Remaining gaps

- No native (Tauri) run: the Playwright screenshots use the existing IPC
fixture. Native import, publisher continuity and the deep-link handoff
were not changed here.
- No human has tried it in the app yet, so `buzz-review-completed` is
not set.

## Initiative context

Status: https://g2.sqprod.co/apps/kylez-buzz-improvements-status
(buzz-improvements T027).

Fixes NEXUS-4327

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Kyle Zemek <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
@joahg
joahg requested a review from kazemek21 November 9, 2026 22:25
@joahg
joahg force-pushed the joah/packaged-managed-plugins branch from 8c279d6 to 999cd38 Compare November 9, 2026 23:38
joahg and others added 3 commits November 9, 2026 16:05
Until the first fetch succeeds there is no stored copy, so a failed fetch
recommended nothing and said nothing. The manager now records each fetch's
outcome, and the managed status reports `loading` or `unavailable` (with the
reason) when no copy exists. Settings shows either state, offers Retry
through a new `plugin_managed_refresh` command, and retries once when
opened after a failure. A failed refresh keeps the last good copy.

Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Joah Gerstenberg <[email protected]>
`plugin_managed_refresh` was missing from the app manifest, the default
capability and the browser permission list, so Retry could not reach it.

The last good packaged policy is now stored as
`packaged-managed-plugins-<hash of the URL>.json`. A build that names another
URL never shows a copy fetched from a different one, and the first usable
policy from the new URL deletes the copies kept for other URLs.

Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Joah Gerstenberg <[email protected]>
@joahg
joahg force-pushed the joah/packaged-managed-plugins branch from 999cd38 to 3fdda06 Compare November 10, 2026 00:08
@tellaho

tellaho commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

🤖 Could you add a screenshot or short video of the change to the PR? Design uses these for faster reviews.

@joahg

joahg commented Nov 11, 2026

Copy link
Copy Markdown
Member Author

🤖 Here's the plugin settings change, recorded in Buzz's browser test harness with simulated desktop IPC. The policy URL and its fetch results are example data: first loading, then a timeout, then a successful Retry.

demo-2504.mp4

While the first fetch is running, Settings → Manage plugins says the recommended list is loading:

Recommended plugins section reading "Loading the recommended plugin list…"

If the first fetch fails and there's no saved copy, the section names the URL and the reason, and offers Retry. Opening Settings in this state also retries once on its own:

Recommended plugins unavailable, with the policy URL, the timeout reason and a Retry button

After a fetch succeeds, the recommended list looks the same as it does today for a managed policy file:

Recommended by Example Org section listing a plugin with Review and install

The loading and unavailable states follow the layout of the existing "Recommended plugins unavailable" state for an invalid policy file. That puts the text at the section's edge rather than under the header's inset. I kept that to match main, but I'm happy to inset all three if design prefers.

@joahg
joahg merged commit dff2a9b into block:main Nov 11, 2026
64 checks passed
joahg added a commit to joahg/buzz-app that referenced this pull request Nov 11, 2026
* origin/main:
  feat(messages): let attachment renderers open files from the host file card (block#2239)
  feat(charts): bundle a Charts code-block renderer (block#3226)
  Expose native reply grouping through Conversation.ui (block#3245)
  NEXUS-4163: Keep plugin-owned chat deletion in Solo (block#2787)
  NEXUS-4156: Expose Solo Workspaces to plugin regions (block#2786)
  NEXUS-4022: Start region chats in the New Chat editor (block#2784)
  feat(link-previews): unfurl web links below messages by default (block#3225)
  fix(messages): keep names in literal Markdown as written when editing (block#2834)
  feat(plugins): fetch a packaged managed plugin policy from a URL (block#2504)

Signed-off-by: Joah Gerstenberg <[email protected]>

# Conflicts:
#	crates/plugin-manager/tests/management.rs
#	docs/plugin-architecture.md
bostonaholic added a commit that referenced this pull request Nov 11, 2026
…y-untagged-reactions

* origin/main: (324 commits)
  fix(github): show private-repo body images from signed links (#3252)
  NEXUS-4029: Preserve semantic chips in the core composer (#2303)
  Make plugin signature enforcement opt-in (#2212)
  NEXUS-4156: Add workspace Leads and choose a chat's workspace (#2788)
  feat(plugins): report conclusively retired update providers (#3263)
  feat(messages): let attachment renderers open files from the host file card (#2239)
  feat(charts): bundle a Charts code-block renderer (#3226)
  Expose native reply grouping through Conversation.ui (#3245)
  NEXUS-4163: Keep plugin-owned chat deletion in Solo (#2787)
  NEXUS-4156: Expose Solo Workspaces to plugin regions (#2786)
  NEXUS-4022: Start region chats in the New Chat editor (#2784)
  feat(link-previews): unfurl web links below messages by default (#3225)
  fix(messages): keep names in literal Markdown as written when editing (#2834)
  feat(plugins): fetch a packaged managed plugin policy from a URL (#2504)
  fix(goose): enable deferred tool loading for goose agents (#3227)
  fix(unread): preserve manual intent in captured explicit reads (#3228)
  NEXUS-4337: Expose retained plugin update reviews (#3075)
  feat(conversation): add a link preview slot below messages (#3219)
  feat(plugins): let link previews read a public page's preview fields (#3217)
  Let sidebar row plugins decorate ordinary Solo chats (#3222)
  ...
tlongwell-block added a commit that referenced this pull request Nov 11, 2026
* origin/main: (121 commits)
  Expose Solo region state before a component mounts (#3296)
  fix(builderlab): say a 401 was refused, not expired, and show each sign-in failure once (#3241)
  feat(agents): file new Solo chats in explicit personal workspaces (#2857)
  perf(css): limit broad style invalidation (#3267)
  Delay and fade in the Bestie badge celebration (#3292)
  fix(github): show private-repo body images from signed links (#3252)
  NEXUS-4029: Preserve semantic chips in the core composer (#2303)
  Make plugin signature enforcement opt-in (#2212)
  NEXUS-4156: Add workspace Leads and choose a chat's workspace (#2788)
  feat(plugins): report conclusively retired update providers (#3263)
  feat(messages): let attachment renderers open files from the host file card (#2239)
  feat(charts): bundle a Charts code-block renderer (#3226)
  Expose native reply grouping through Conversation.ui (#3245)
  NEXUS-4163: Keep plugin-owned chat deletion in Solo (#2787)
  NEXUS-4156: Expose Solo Workspaces to plugin regions (#2786)
  NEXUS-4022: Start region chats in the New Chat editor (#2784)
  feat(link-previews): unfurl web links below messages by default (#3225)
  fix(messages): keep names in literal Markdown as written when editing (#2834)
  feat(plugins): fetch a packaged managed plugin policy from a URL (#2504)
  fix(goose): enable deferred tool loading for goose agents (#3227)
  ...

Signed-off-by: Tyler Longwell <[email protected]>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@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