Repository navigation
Admin plugin install from the directory (#553) - #570
Merged
Merged
Conversation
…ssions (#553) Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…joined Init errors (#553) Init no longer stops at the first failing OnInit (#569). Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…ectory plugins (#553) Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
… downloads (#553) Fix round 1 from review: hold a mutex across Install/Update/Uninstall so a concurrent call on the same name can't delete the winner's file; add a hookBeforeRegister test seam to actually exercise Update's rename -> write -> register -> rollback path (the existing test only hit the earlier, pre-write rejection); validate plugin names against the directory's ValidName before they reach a filesystem path; refuse HTTP redirects off https/loopback; force a re-fetch when the configured index URL changes instead of waiting out Ensure's throttle; add ErrUpToDate/ErrDownload sentinels; and clean up a leaked temp file on a failed atomic rename. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
) Status() now drops index entries with an invalid name (same check lookup() already applies) and strips any source_url/download_url that isn't an absolute http(s) URL, so a hostile plugin_directory_url can't smuggle a javascript: link or an unsafe name into the admin page. The Plugins page stops building onclick/onchange from directory data (entity-decoding happens before evaluation) in favor of data-action/ data-name attributes read by delegated listeners, and only renders Source/Directory-page links whose value passes the same scheme/name checks client-side. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Address the final whole-branch review's two Important findings: - ensureIndex only forced a refresh when the directory URL changed, so Fetcher.Ensure's "fetch once while empty" behaviour meant an already- cached index (and thus "update available") never refreshed on its own. Add a staleAfter threshold and refresh once the cache is older than that. - An unwritable or missing plugins/dynamic/ surfaced as a generic 500. Add ErrWrite, wrap the installer's write/rename/remove failures in it, map it to 507 in the admin API, and have Status() probe writability so the admin page can explain the problem and disable Install instead of letting every action fail. Also fixes the minors from the same review: a non-semver installed version now gets an accurate "not semver" message instead of "already at the directory's version" on Update; a comment explains why the update rollback re-runs OnInit; the Uninstall remove failure reuses ErrWrite; the admin page's plugin-action buttons now always reset even if the follow-up status reload fails; and the "requires goblog >= X" text is only shown when the plugin actually declares a minimum version. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
) Docs follow-ups from the final whole-branch review: hello.go.example and the README both said dynamic plugins are only interpreted "at startup", which is no longer true now that Admin -> Plugins can load one at runtime. Also spell out that plugin_directory_url is a trust decision (whatever it points at can run code inside goblog) and that an update's old interpreter instance stays around harmlessly until restart. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces runtime third-party code installation/execution and significant registry lifecycle changes that warrant careful human security/operational review despite test coverage.
Pull request overview
Adds end-to-end support for installing dynamic plugins directly from a remote plugin directory via the admin UI, including registry lifecycle changes needed for hot install/update/uninstall and directory UX improvements (stars sorting + submission link).
Changes:
- Extend the plugin registry to support dynamic plugin registration/unregistration, per-plugin job stop channels, and non-blocking Init with joined errors.
- Introduce
plugin/installerto fetch + verify + load + atomically write + hot-register plugins, and wire it through new admin API endpoints +/admin/pluginsUI. - Enhance the public plugin directory listing with stars sorting and a “Submit your plugin” box; document new behavior in README/specs.
File summaries
| File | Description |
|---|---|
| tools/migrate.go | Seeds new default site setting plugin_directory_url. |
| themes/default/templates/admin_plugins.html | New Admin → Plugins page (Installed/Browse tabs) with delegated JS handlers. |
| themes/default/templates/admin_nav.html | Adds “Plugins” link to admin nav. |
| README.md | Documents one-click install/update/uninstall from the directory and related requirements. |
| plugins/dynamic/hello.go.example | Updates dynamic plugin example docs to mention admin install path. |
| plugins/directory/templates/listing.html | Adds stars column and “Submit your plugin” form/script to directory listing page. |
| plugins/directory/render.go | Exposes ValidName for shared name validation use. |
| plugins/directory/index.go | Adds Stars field to directory index entries. |
| plugins/directory/directory.go | Sorts directory listing by stars desc then name asc before rendering. |
| plugins/directory/directory_test.go | Updates escaping test and adds coverage for stars sort + submit box. |
| plugin/validate.go | Switches validation to use exported dynamic loader. |
| plugin/registry.go | Implements dynamic registry lifecycle (RegisterDynamic/Unregister/InitPlugin/DeleteSettings/Dynamic) and joined Init errors. |
| plugin/registry_test.go | Adds tests for unregister stopping jobs, Init continuing past failures, InitPlugin/DeleteSettings. |
| plugin/loader.go | Exports LoadDynamicPlugin/LoadDynamicPluginBytes and registers dynamic plugins with paths. |
| plugin/loader_test.go | Adds tests for LoadDynamicPluginBytes. |
| plugin/installer/version.go | Implements minimal semver parsing/compare for compatibility + update checks. |
| plugin/installer/version_test.go | Tests semver compatibility/newer helpers. |
| plugin/installer/installer.go | Core installer: index caching/refresh, HTTPS download rules, checksum verification, atomic writes, hot register/update rollback, uninstall. |
| plugin/installer/installer_test.go | Installer integration tests (install/update/rollback/concurrency/sanitization/writability). |
| goblog.go | Wires installer into runtime, logs joined init errors, adds admin routes and page route. |
| docs/superpowers/specs/2026-09-17-plugin-install-admin-design.md | Design spec for admin plugin install + directory stars/submissions. |
| docs/superpowers/plans/2026-09-17-plugin-install-admin.md | Implementation plan and acceptance criteria reference. |
| blog/blog.go | Adds SettingValue(key, def) helper for safe settings lookup with defaulting. |
| blog/blog_test.go | Adds tests for SettingValue. |
| admin/plugins.go | Implements plugin admin API handlers + AdminPlugins page renderer and error mapping. |
| admin/plugins_test.go | Adds tests for plugin API behavior and admin plugins page template constraints. |
| admin/admin.go | Adds Installer *installer.Installer field to Admin. |
Review details
- Files reviewed: 27/27 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
#553) Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This was referenced Sep 18, 2026
compscidr
added a commit
to goblogplatform/plugins
that referenced
this pull request
Sep 18, 2026
Without SUBMIT_TOKEN the org blocks Action-created PRs, so a passing submission previously wedged: it pushed a branch, failed at gh pr create, posted no comment, and every re-run then failed to push (non-fast-forward). Guard on HAS_TOKEN to comment and stop before pushing when the secret is unset, force-with-lease the bot branch push, and comment (then exit 1) if PR creation still fails. Document SUBMIT_TOKEN as required, from a dedicated machine identity. Sanitize submitted-plugin output before echoing it into issue comments (zero-width space after '@', modifier-letter grave for backticks, a ~~~~ fence) so a submission can't inject mentions or close the fence; cut the 6000-byte cap on a line boundary. Give empty-REPO parses a friendly comment instead of the generic failure text. Tighten the owner half of the repo pattern to exclude '.', matching goblogplatform/GoBlog#570; apply the same tightening to internal/registry/registry.go's repoPattern. Reject submission names that are '.'/'..' or still end in '.git' after the URL strip. Add a submit-scoped concurrency group, persist-credentials: false on check's checkout, and a build test covering a RepoInfo failure being skipped rather than failing the whole build. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #553, closes #569.
RegisterDynamic/Unregister/InitPlugin/DeleteSettings/Dynamic,Initcontinues past a failingOnInitand returns the joined errors (Registry.Init stops at the first plugin whose OnInit fails #569); Yaegi loader exported.plugin/installer— download (https only; 1 MB cap; redirect check) → sha256 vs the index → Yaegi load →Name()/Version()must match → atomic write → hot register. Update with.prevrollback, uninstall removes file + settings, mutex-serialised, typed errors, index refreshed on URL change and after 1h, writability probe./api/v1/plugins/*: Installed tab (update available, uninstall, enable toggle) and Browse tab (search; sort by stars / name / newest; one-click Install). Banners whenENABLE_DYNAMIC_PLUGINSis off, the directory is unreachable, orplugins/dynamic/isn't writable; a trust notice. Directory data is sanitised server-side and rendered via data-attributes + delegated handlers (no data in inline JS).plugin_directory_url(defaulthttps://www.goblog.live/plugins/index.json).starsfield, populated by the registry build in a follow-up), and a Submit your plugin box that opens a pre-filled issue ongoblogplatform/plugins.docs/superpowers/specs/2026-09-17-plugin-install-admin-design.md.Follow-ups (separate PRs):
goblogplatform/plugins—starsin the index + submission issue template/workflow;goblog-site-theme—admin_plugins.html+ nav link; iac —ENABLE_DYNAMIC_PLUGINS=trueand aplugins/dynamicbind mount for goblog.live.Test plan
go build -v . && go vet ./... && go test -race goblog/...(installer/registry tests include rollback, concurrent install, redirect and read-only-dir cases)ENABLE_DYNAMIC_PLUGINS=true, installedhellofrom the live directory via the API — greeting appeared, file written; uninstalled; without the env var → 412 + banner state🤖 Generated with Claude Code