Skip to content

Plugin collection manifests and nested git installs - #1579

Open
SawyerHood wants to merge 27 commits into
mainfrom
marketplace-collection-manifest
Open

Plugin collection manifests and nested git installs#1579
SawyerHood wants to merge 27 commits into
mainfrom
marketplace-collection-manifest

Conversation

@SawyerHood

@SawyerHood SawyerHood commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Bottom layer of the plugin marketplace stack (1/4). Implements #1097.

Summary

  • Adds the repository collection manifest .bb/plugins.json: a strict, versioned index (schemaVersion: 1) naming nested plugins by relative path, with duplicate-name, traversal, root-source, and symlink-containment validation. The JSON schema is published at apps/web/public/schemas/plugins.schema.json.
  • Adds nested installs: bb plugin install git:<url>[@ref] --subdirectory <path> as the primitive and --plugin <name> to resolve a manifest entry; both also work for path: sources. An unselected install into a manifest-bearing repo with no root plugin fails listing the available entries.
  • Persists the subdirectory end to end (sourceGitSubdirectory was already plumbed through updates/rollback; installs now fill it), and makes plugins from one repo@commit share a single cached checkout — promotion moves only the selected subdirectory, with crash recovery, GC protection for live siblings, and refreshed ancestor hashes.
  • Ships the marketplace design doc (docs/plugin-marketplace-plan.md) that the rest of the stack implements.
  • Surfaces updated in the same change: CLI flags/help, install route selection contract, SDK, bb guide, bb-cli and bb-plugin-authoring skills.

Validation

Turbo typecheck workspace-wide; server/app/cli/db suites green on this branch standalone (one known machine-local umask failure in internal-skill-trees.test.ts). New tests cover schema validation, symlinked manifests/entries, shared-checkout promotion with sibling integrity, nested update checks, and rollback.

Reviewed by a single adversarial pass plus a whole-stack final review; findings were fixed in-branch.

AGENT GENERATED: by Claude Fable 5

@SawyerHood SawyerHood changed the title marketplace collection manifest Plugin collection manifests and nested git installs Aug 14, 2026
@SawyerHood
SawyerHood marked this pull request as ready for review August 14, 2026 05:29
@SawyerHood
SawyerHood force-pushed the marketplace-collection-manifest branch from 631e740 to 56ae0e9 Compare August 14, 2026 06:28

@bb-slop-cop bb-slop-cop 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.

🚨 SLOP COP 🚨 · \n\nI am the SlopCop. I am reviewing this pull request now.\n\nI will check security, code quality, architecture, performance, and the main user workflow.\n\n

@bb-slop-cop bb-slop-cop 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.

🚨 SLOP COP 🚨 · review

I am the SlopCop. I am reviewing this pull request now.

I will check security, code quality, architecture, performance, and the main user workflow.

return null;
}
const parts = artifact.path.split(sep);
const commitIndex = parts.lastIndexOf(artifact.gitResolvedCommit);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 slopcop/review — High: Cleanup can remove files from an active root plugin.

lastIndexOf can select a nested directory that has the same name as the commit.

The tenant query then misses the active artifact at the real checkout root.

Store the checkout root with each Git artifact. Use that value for tenant checks and removal.

contentHash,
preserveNestedRoots: preservedNestedRoots(stagedTargetRoot),
});
await refreshAncestorArtifactHashes({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 slopcop/review — Medium: A process stop can leave an ancestor artifact hash stale.

Promotion changes the shared checkout before this hash update starts.

Startup recovery repairs promotion directories, but it does not repair these hashes.

During startup, recompute the pending artifact hash and each affected ancestor hash.

name: "beta",
});

expect(entry.rootDir).toBe(join(repoDir, "plugins", "beta"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 slopcop/review — Low: This new test fails on macOS.

realPathInside returns the canonical /private/... path, while repoDir keeps the /var/... path.

The focused server run reproduced this failure.

Use realpath for the two expected nested path values.

"plugin install accepts subdirectory or plugin, not both",
);
}
const body = pluginInstallSourceRequestSchema.parse({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 slopcop/review — Low: The SDK silently removes unknown install fields.

This code creates a new object before the strict schema parses the input.

A JavaScript caller can misspell plugin, and the SDK will select the root.

First parse the original input with a strict local schema.

// An identical target is settled before anything moves: `promoteImmutableDir`
// drops the staging tree in that case, and the carried-over plugins are in
// it by then.
if (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 slopcop/review — Low: Nested promotion reads the same target tree twice.

This hash proves a mismatch before promoteImmutableDir immediately hashes the unchanged tree again.

A root plugin can make both reads cover a large shared checkout.

Pass the known mismatch to the helper, or combine the two checks.

@bb-slop-cop bb-slop-cop 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.

🚨 SLOP COP 🚨 · review

This PR lets one repository list several plugins. Users can select a named plugin or a folder during path and Git installs.

It also updates installation, updates, recovery, and cleanup for shared Git checkouts.

I found these issues:

  • High: Cleanup can derive the wrong checkout root and remove files from an active root plugin.
  • Medium: A process stop after promotion can leave an ancestor artifact hash stale.
  • Medium: Update checks can clone one collection repository once per plugin at the same time.
  • Low: The new local collection test fails on macOS because it compares canonical and noncanonical paths.
  • Low: The SDK removes unknown install fields before strict validation.
  • Low: Nested promotion reads the same large target tree twice.

I found no actionable security issue. The new path and link checks protect the checkout boundary.

The CLI, SDK, and app checks passed 69 tests. The server checks passed 74 of 75 tests.

The one server failure was the macOS path test above. All GitHub checks passed.

The live browser test also passed. The UI explained required collection selection, the CLI installed one nested plugin, and the UI showed it.

The cleanup root needs explicit artifact data. That change also supports grouped cleanup queries and removes brittle path parsing.

SawyerHood and others added 18 commits August 14, 2026 15:52
Design doc for the collection manifest (#1097), the marketplace manifest
format, the official registry repo and R2 hosting, git tag semver ranges,
and the in-app submission flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nifest

Add `.bb/plugins.json`, a strict repository-level index of nested plugin
directories, and the install flags that select one of them:
`bb plugin install git:<url>[@<ref>] --subdirectory <path>` is the primitive
and `--plugin <name>` resolves a collection entry. Both flags also work for
`path:` sources. Sources must start with "./"; absolute paths, "..", empty
segments, duplicate names, the repository root, and symlinks that leave the
checkout are rejected.

The install now fills `sourceGitSubdirectory` end to end, so outdated,
update, rollback, and remove act per plugin. One repository and commit keep a
single checkout: the artifact hash and promotion cover the plugin root, so a
second nested install never replaces a sibling's dependencies and bundles,
and artifact GC of a nested plugin removes only its own directory.

Ships the CLI flags, the `selection` field on POST /plugins/install, the SDK
`subdirectory` / `plugin` install arguments, the guide chapter, the bb-cli and
bb-plugin-authoring skills, and docs/configuration.md.

Refs #1097

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…moted

A repository and commit share one checkout, so a plugin root can contain
another plugin's root. Promotion moved only the selected subdirectory, which
covered a nested install but not the reverse: installing or updating the
repository root replaced the whole checkout, and the dependencies and bundles
a nested sibling had built into it were deleted. The sibling kept running
against a plugin root that no longer had a dist bundle.

Promotion now carries the built tree of every plugin root inside the promoted
root over from the target, and reports the content hash of what actually
landed so the artifact row stays consistent with the disk. A failed promote
moves the carried trees back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Artifact GC of a nested plugin removed only its own directory, but GC of a
plugin installed from the repository root still removed the whole shared
checkout. That deleted the plugin root of every nested sibling of the same
commit, including plugins that are installed and running.

GC now skips an artifact while another artifact is stored inside its storage
root, so the checkout goes away on the pass that follows its last tenant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An update check cloned the candidate into a staging directory next to the
plugin root. For a nested plugin that root is a directory of the shared
checkout, so the clone landed inside the checkout — inside the plugin root of
a repository-root plugin installed from the same commit. That changed the
root plugin's content hash while the check ran, and a crash left a full clone
in a live plugin root. The staging directory is now a sibling of the checkout
in both cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SawyerHood and others added 8 commits August 14, 2026 15:52
The rebase onto main resolved the bundled SDK declaration conflict by
taking main's copy, which dropped this layer's `subdirectory` and
`plugin` collection-source fields. `@bb/templates` typecheck and test
both gate on `generate-templates.mjs --check`, so both went red.

Rebuild @get-bb/plugin-sdk and regenerate the templates copy.
The stack extends the published SDK types (collection selection, catalog
marketplace provenance, git range sources); 0.4.4 is already on npm and
is never republished, so the npm version guard requires a bump.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Garbage collection derived the shared checkout root by searching the
artifact path for the resolved commit. A repository that holds a nested
directory with the commit name moved that root below the real checkout.
The tenant query then missed the active root plugin, and cleanup removed
part of a live plugin tree.

plugin_artifacts now stores git_checkout_root. Installs and updates write
the checkout directory, and GC asks for tenants by that exact value.
Migration 0094 backfills existing rows from the first commit component in
the path: for the real cache layout that component is unique, and where a
nested name repeats it, the first one is the true root.
macOS resolves the temporary directory through /private, so the service
records a canonical path that the literal expectation cannot match.
@SawyerHood
SawyerHood force-pushed the marketplace-collection-manifest branch from dc8dd45 to 2997d0b Compare August 14, 2026 16:09
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.

1 participant