Plugin collection manifests and nested git installs - #1579
Conversation
631e740 to
56ae0e9
Compare
| return null; | ||
| } | ||
| const parts = artifact.path.split(sep); | ||
| const commitIndex = parts.lastIndexOf(artifact.gitResolvedCommit); |
There was a problem hiding this comment.
🚨 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({ |
There was a problem hiding this comment.
🚨 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")); |
There was a problem hiding this comment.
🚨 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({ |
There was a problem hiding this comment.
🚨 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 ( |
There was a problem hiding this comment.
🚨 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.
There was a problem hiding this comment.
🚨 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.
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>
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.
dc8dd45 to
2997d0b
Compare
Bottom layer of the plugin marketplace stack (1/4). Implements #1097.
Summary
.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 atapps/web/public/schemas/plugins.schema.json.bb plugin install git:<url>[@ref] --subdirectory <path>as the primitive and--plugin <name>to resolve a manifest entry; both also work forpath:sources. An unselected install into a manifest-bearing repo with no root plugin fails listing the available entries.sourceGitSubdirectorywas 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.docs/plugin-marketplace-plan.md) that the rest of the stack implements.selectioncontract, 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.