Skip to content

Admin plugin install from the directory (#553) - #570

Merged
compscidr merged 12 commits into
mainfrom
feat/553-plugin-install
Sep 18, 2026
Merged

compscidr merged 12 commits into
mainfrom
feat/553-plugin-install

Conversation

@compscidr

Copy link
Copy Markdown
Collaborator

Summary

Closes #553, closes #569.

  • Plugin registry lifecycle — per-plugin stop channels, RegisterDynamic / Unregister / InitPlugin / DeleteSettings / Dynamic, Init continues past a failing OnInit and 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 .prev rollback, uninstall removes file + settings, mutex-serialised, typed errors, index refreshed on URL change and after 1h, writability probe.
  • Admin → Plugins page + /api/v1/plugins/*: Installed tab (update available, uninstall, enable toggle) and Browse tab (search; sort by stars / name / newest; one-click Install). Banners when ENABLE_DYNAMIC_PLUGINS is off, the directory is unreachable, or plugins/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).
  • New site setting plugin_directory_url (default https://www.goblog.live/plugins/index.json).
  • Public directory page: sorted by stars (stars field, populated by the registry build in a follow-up), and a Submit your plugin box that opens a pre-filled issue on goblogplatform/plugins.
  • Spec: docs/superpowers/specs/2026-09-17-plugin-install-admin-design.md.

Follow-ups (separate PRs): goblogplatform/plugins — stars in the index + submission issue template/workflow; goblog-site-theme — admin_plugins.html + nav link; iac — ENABLE_DYNAMIC_PLUGINS=true and a plugins/dynamic bind 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)
  • Manual: ENABLE_DYNAMIC_PLUGINS=true, installed hello from the live directory via the API — greeting appeared, file written; uninstalled; without the env var → 412 + banner state

🤖 Generated with Claude Code

compscidr and others added 11 commits September 17, 2026 11:01
…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]>
)

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]>
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

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Copilot AI 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.

🔵 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/installer to fetch + verify + load + atomically write + hot-register plugins, and wire it through new admin API endpoints + /admin/plugins UI.
  • 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.

Comment thread plugin/installer/installer.go Outdated
Comment thread themes/default/templates/admin_plugins.html Outdated
@compscidr
compscidr merged commit 58c6257 into main Sep 18, 2026
1 check passed
@compscidr
compscidr deleted the feat/553-plugin-install branch September 18, 2026 00:50
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]>
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.

Registry.Init stops at the first plugin whose OnInit fails Admin UI: search, install and update plugins from the plugin directory

2 participants