Keep plugin SDK declarations current so agents stop reading bundles - #1107
Conversation
SawyerHood
left a comment
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
I am SlopCop. I am reviewing this change for security, code quality, performance, architecture, and end-to-end behavior.
The stack contains only #1107. I did not skip a pull request.
| continue; | ||
| } | ||
| await mkdir(typesDir, { recursive: true }); | ||
| await writeFile(filePath, candidate.content); |
There was a problem hiding this comment.
🚨 slopcop/review — High: Refuse symbolic links before declaration writes.
writeFile follows this path. A plugin repository can commit types/bb-plugin-sdk.d.ts as a symbolic link to any writable file. Then bb plugin types, bb plugin build, or bb plugin dev replaces that external file. A linked types/ directory has the same effect.
I confirmed this behavior with the pull request CLI. The command replaced a file outside types/ through a declaration link.
Reject linked directories and destination files. Verify that the real destination remains below the real plugin root. Then write through a safe temporary regular file and rename it. Add tests for both link forms.
10abb40 to
60fe4a4
Compare
SawyerHood
left a comment
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
I am SlopCop. I am reviewing this pull request for security, code quality, performance, architecture, and end-to-end behavior.
Stack: #1107 only.
No pull request was skipped.
SawyerHood
left a comment
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
Performance phase complete for #1107.
No performance findings exist. The agent confirmed unchanged declarations keep their modification time. Synchronization runs once before the development watcher starts.
The CLI and template tests passed. The type checks passed. One external scaffold test could not run npm pack; it showed no performance regression.
SawyerHood
left a comment
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
Security phase complete for #1107.
One high-severity finding exists. The new automatic declaration refresh follows symbolic links. A hostile plugin can replace a user-writable file outside its plugin directory.
The agent confirmed the exact head SHA. The focused tests and type checks passed.
| continue; | ||
| } | ||
| await mkdir(typesDir, { recursive: true }); | ||
| await writeFile(filePath, candidate.content); |
There was a problem hiding this comment.
🚨 slopcop/review — Reject symbolic links before the automatic declaration write.
writeFile follows a symbolic link at the declaration file or its types directory. A hostile plugin can link types/bb-plugin-sdk.d.ts to another user-writable file. The new bb plugin build and bb plugin dev paths then replace that file automatically.
I reproduced this behavior with a temporary plugin. The declaration write replaced a file outside the plugin root.
Use lstat to reject links. Resolve the real destination and verify that it stays inside the real plugin root. Use a temporary file and an atomic rename.
SawyerHood
left a comment
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
End-to-end phase complete for #1107.
I built the repository CLI. I created a real plugin scaffold. The initial bb plugin types --check passed.
I changed the declaration. The check then failed with the expected stale result. The refresh wrote the current declaration. The next check passed.
After dependency installation, bb plugin build passed. A browser test does not apply because this change adds no HTTP route or user interface.
SawyerHood
left a comment
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
Code quality and architecture phase complete for #1107.
One low-severity finding exists. The new synchronization function duplicates the declaration writes that remain in scaffoldPlugin.
No other correctness, contract, CLI behavior, edge-case, or stale-name findings exist.
The CLI and template type checks passed. My full template suite passed all 19 tests.
| * Files are compared before writing, so an already-current plugin reports | ||
| * `unchanged` and keeps its mtime. | ||
| */ | ||
| export async function syncPluginTypes( |
There was a problem hiding this comment.
🚨 slopcop/review — Use the synchronization function for the initial scaffold writes.
syncPluginTypes duplicates the direct declaration writes that remain later in scaffoldPlugin. A future declaration change can make new plugins and refreshed plugins receive different contracts.
Call syncPluginTypes({ rootDir: targetDir, app }) from scaffoldPlugin. Remove the direct declaration writes.
| // Keep the local declarations tracking the bb doing the build, so a | ||
| // plugin scaffolded against an older SDK never typechecks green | ||
| // against an API this bb no longer has. | ||
| if (manifest) await refreshPluginTypes(rootDir, hasApp); |
There was a problem hiding this comment.
🚨 slopcop/review — Validate the plugin before the declaration refresh changes its files.
A readable package.json without bb.server reaches this write. buildPluginServer rejects the package only after the refresh.
I reproduced this with an empty manifest. bb plugin build failed as expected, but it first created types/bb-plugin-sdk.d.ts.
Require a string bb.server before this call, or perform all manifest validation before any write.
SawyerHood
left a comment
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
Final review for stack #1107.
One high-severity security finding blocks a safe merge.
- The declaration write follows symbolic links.
- A hostile plugin can replace a writable file outside its root.
bb plugin buildandbb plugin devnow start this write automatically.
Two low-severity quality findings also exist.
- A failed build writes declarations before it validates
bb.server. - The scaffold and refresh paths duplicate the declaration write policy.
I found no performance issue.
Validation passed:
- Turbo type checks for
@bb/cliand@bb/templates. - All 19 template tests.
- The CLI build.
- The complete
bb plugin typesstale and refresh flow. - The plugin build after dependency installation.
- All GitHub checks.
A browser test does not apply because this change adds no route or user interface.
I posted three inline findings. This review uses comment status only. It does not approve or request changes on GitHub.
`bb plugin new` seeds types/*.d.ts once and nothing ever refreshes them, so every plugin scaffolded against an older bb typechecks green against an API that has since moved. Local copies here ranged from 130 to 1,900 lines behind the current 12,868-line surface. With no trustworthy local source of truth, an agent looking for an exact signature falls back to grepping minified build output. Add `bb plugin types [path]`: it writes the running bb's @bb/plugin-sdk declarations into the plugin's types/, creating the directory when absent, and needs no server. `--check` reports staleness and exits non-zero without writing, for CI. `bb plugin build` and `bb plugin dev` run the same sync automatically; dev refreshes before its watcher starts so the write cannot feed the loop its own change event, and a failure warns rather than failing a build. The shared syncPluginTypes sits next to the scaffold that seeds the same files. It compares before writing, so a current plugin reports `unchanged` and keeps its mtime, and a headless plugin never gets a frontend declaration it did not ask for. The bb-plugin-authoring skill gains a "Looking up the exact API" section: run `bb plugin types`, read the .d.ts, clone the repo — with an explicit rule never to answer an API question from a built bundle. Guide chapter, bb-cli skill, and scaffold README updated to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same ladder and the same prohibition, roughly half the words. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three fixes from the SlopCop review of this PR. `writeFile` follows links, so a plugin that shipped `types/` — or a declaration inside it — as a symbolic link redirected the write onto whatever it pointed at. That was already reachable through `bb plugin types`, and the new automatic refresh made `bb plugin build` and `bb plugin dev` do it without the author asking. Building a plugin never runs its code, so cloning an untrusted plugin and building it must not write anywhere but that plugin. I reproduced the clobber before fixing it. syncPluginTypes now lstats without following, refuses either link form, verifies the resolved `types/` still sits inside the resolved plugin root, and writes through a temporary regular file it renames into place. Tests cover both link forms and assert the target survives. Also: scaffoldPlugin now seeds declarations through syncPluginTypes instead of duplicating the writes, so a scaffolded plugin and a refreshed one cannot diverge; and `bb plugin build` gates the refresh on a string `bb.server`, so a directory it is about to reject is never written to first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
471bfc1 to
d2d96a4
Compare
The problem
bb plugin newseedstypes/*.d.tsonce, and nothing ever refreshes them. The SDK surface grows every release, so a plugin scaffolded against an older bb typechecks green against an API that has since moved. Local copies on one machine:bb-plugin-posthog-dashboardomegacodebb-slop-copbb-plugin-strudelThe
bb-plugin-authoringskill points agents attypes/for an exact signature, but could not promise the directory existed or was current. With no trustworthy local source of truth, an agent falls back to grepping minified build output — which costs far more context and answers worse.The fix
bb plugin types [path]writes the running bb's@bb/plugin-sdkdeclarations into the plugin'stypes/, creating the directory when absent. Needs no server.--checkreports staleness and exits non-zero without writing, for CI.Auto-refresh:
bb plugin buildandbb plugin devrun the same sync.devrefreshes before its watcher starts, so the write cannot feed the loop its own change event. A failure warns and continues — an unwritabletypes/must not fail a build.syncPluginTypeslives inpackages/templates/src/plugin-scaffold.ts, next to the scaffold that seeds the same files. It compares before writing, so a current plugin reportsunchangedand keeps its mtime, and a headless plugin never gets a frontend declaration it did not ask for.The skill gains a
## Looking up the exact APIsection with a three-step ladder — runbb plugin types, read the.d.ts, clone the repo with a copy-pasteablegit clone --depth 1— plus an explicit rule: never answer an API question from a built bundle, and if you catch yourself grepping minified JavaScript, go back to step 1. A Gotchas bullet points back at it.Guide chapter (regenerated),
bb-cliskill, and scaffold README updated to match.Verification
Against the real stale posthog-dashboard plugin:
--checkreported both files stale and exited 1;bb plugin typestook the root declaration 10,966 → 12,868 lines and the app declaration 507 → 1,443; a re-check reportedunchanged. A headless plugin with notypes/got only the root declaration.bb plugin buildprintedRefreshed SDK declarationsbefore compiling.pnpm exec turbo run typecheck test --filter=@bb/cli --filter=@bb/templatespasses (387 tests). Server skill and plugin-authoring-doc tests pass (75 tests). Five new tests inpackages/templates/test/plugin-sync-types.test.tscover stale replacement, unchanged-keeps-mtime, the headless case, refreshing an existing app declaration, and check-mode writing nothing.One call to confirm
bb plugin buildnow rewritestypes/in place, so a plugin that commits those files and builds in CI will show a dirty tree. That is deliberate —--checkis the non-writing alternative — but happy to makebuildwarn instead.🤖 Generated with Claude Code