Repository navigation
feat(plugins): fetch a packaged managed plugin policy from a URL - #2504
Conversation
…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]>
235f7f3 to
3d2d364
Compare
tellaho
left a comment
There was a problem hiding this comment.
🤖 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.
| let wait = match refresh(&client, &manager).await { | ||
| Ok(()) => REFRESH, | ||
| Err(error) => { | ||
| eprintln!("Could not refresh the packaged plugin policy: {error}"); |
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 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::effectivereturns{status: "loading", url}or{status: "unavailable", url, reason}instead ofAbsent. Once a copy exists, it still wins, so a failed refresh keeps the last good recommendations. Amanaged-plugins.jsonstill overrides everything. - Retry: a new
plugin_managed_refreshcommand 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_arrivedcovers loading, unavailable, keeping the last good copy after a failure, and the file override.ManagedPlugins.test.tsxcovers 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.
There was a problem hiding this comment.
🤖 [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
There was a problem hiding this comment.
🤖 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.
| 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) { |
There was a problem hiding this comment.
🤖 [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
There was a problem hiding this comment.
🤖 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.
… 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`) |  |  | | After |  |  | ## 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]>
8c279d6 to
999cd38
Compare
Signed-off-by: Joah Gerstenberg <[email protected]>
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]>
999cd38 to
3fdda06
Compare
|
🤖 Could you add a screenshot or short video of the change to the PR? Design uses these for faster reviews. |
|
🤖 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.mp4While the first fetch is running, Settings → Manage plugins says the recommended list is loading: 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: After a fetch succeeds, the recommended list looks the same as it does today for a managed policy file: 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. |
* 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
…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) ...
* 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>



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 writingmanaged-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.
src-tauri/src/packaged_plugins.rs) fetches the URL at startup and every 6 hours, retrying after 15 minutes when an attempt fails.httpsURLs and redirects, times out after 30 seconds, and caps the body at 256 KiB.managed::store_packagedkeeps a response only if it parses as a usable version 1 policy. It writes the copy atomically topackaged-managed-plugins-<hash of the URL>.jsonbeside 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.loading, orunavailablewith the reason after a failure. Settings → Plugins shows either state with a Retry button (plugin_managed_refresh, registered inbuild.rsand the default capability), and opening Settings after a failure retries once.managed::effectiveuses the stored copy only when nomanaged-plugins.jsonexists andBUZZ_MANAGED_PLUGINSdoesn't name a file. A machine manager's file still replaces it completely.build.rsaccepts only anhttpsURL 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-pluginspasses (88, 19 and 31 tests). New tests:reports_a_packaged_policy_that_has_not_arrivedkeeps_each_packaged_copy_for_its_own_urlfalls_back_to_the_stored_packaged_policy_only_without_a_filestores_only_usable_packaged_policiespackaged_managed_plugins_url_accepts_only_plain_httpsvitestforsrc/app/ManagedPlugins.test.tsxandsrc/pluginspasses (1323 acrosssrc/appandsrc/plugins). The new cases cover loading, a first-fetch failure with Retry and retry-on-open followed by recovery, and no retry while busy.tscand biome are clean.cargo test -p Buzz --libpasses exceptconverts_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 withplugin_managed_refresh.cargo fmt --checkpasses.cargo clippy --locked --all-targets -D warningsis clean forbuzzodz-pluginsand the desktop crate.BUZZ_BUILD_MANAGED_PLUGINS_URLset to a live https policy URL:Manager::managedisabsent.store_packaged_policyreturnstrue, and the policy turnsreadywith its source and its one entry.false.version: 2body is refused, and the last good copy stays.managed-plugins.jsonreplaces the packaged copy.loading, thenunavailablewith the reason after a recorded failure.fetchagainst the same URL. Anhttp://URL was refused.🤖 Generated with Claude Code