Repository navigation
feat(plugins): community plugins — sandboxed engine, extension points, in-panel editor - #108
Merged
Merged
Conversation
…in, author tools Plugins are JavaScript that runs inside the master: they react to the panel's events, run on a schedule, keep their own data and reach the panel's /v1 and the internet only as far as the operator agreed on the consent screen. - engine (internal/plugin/jsvm): QuickJS-ng 0.17 built to wasm with a small shim (guest/rp.c, reproducible build, CI rebuild check) and run by wazero; hard memory cap, soft/hard deadlines, no files/env/sockets. Lazy: nothing compiles without a plugin. - own database per plugin (internal/plugin/pdb): its own SQLite file with a quota; ATTACH/VACUUM INTO closed by the attach limit, PRAGMA and transaction statements refused by a lexer (fuzzed). - package and manifest (internal/plugin/manifest): strict validation, zip bomb and path checks; only the points the host calls are accepted (events, cron). - host (internal/plugin): lifecycle with consent, update with snapshot and rollback, breaker, log, panel.* API (kv, db, api, http.fetch with SSRF guard, crypto, t), events through the webhook outbox with their own dispatcher, cron. - panel: /api/plugins with plugins.view/manage, audit, password on install; panel.api runs the real /v1 handlers as the plugin; Settings → Plugins tab. - server events node.down/up and xray.down/up for webhooks and plugins; admin notices for paused plugins. - author tools: rospanel plugin new|validate|pack|test|dev; examples/plugins/ notify-discord; docs/plugins. Migrations 0112-0115. Plan and measurements on the test master: docs/plugins-design.md.
A plugin's payment method is one more provider, plugin.<id>, switched on in Settings → Payments like any other: the panel opens the order, routes the callbacks to payment.webhook, polls payment.status and grants the plan — only when the amount the plugin reports matches the order, which a plugin must always report. onHttp answers at /<callback secret>/x/<id>/…, sandboxed (CSP sandbox, nosniff, no cookies either way). Examples: lava (from the saved spec) and shop-webhook. test.js gets crypto to sign what it sends. Verified on the test master with a fake payment system: forged callback rejected, signed one paid the order, polling confirmed another.
…ge blocks, delivery channel What plugins add to the panel's own screens, all drawn by the panel from plain values: values on a user's card, buttons (on the card, for the users list's selection, on the plugin's card) gated by each button's own permission and journaled, dashboard tiles (cached a minute), blocks at the top of the subscription page (Markdown without raw HTML, https buttons; cached per user) and in the /v1 subscription view as blocks. A delivery channel (experimental) gets, as channel.send, the messages, broadcasts and reminders the panel's bot did not deliver. Fixed on the way: the work behind those events ran only when a webhook wanted them, so a plugin subscribed to one never got it. Examples: email (Resend) and announcement. Verified on the test master.
…ion rewrites Plugins now take part in the panel's decisions: beforeSignup on every sign-up path (API, Mini App, bot), beforeDeviceBind for a device not bound yet, and quotePrice for a user's plan price, held to half to all of the base. All of them fail open within 0.3 s, never wait for a busy plugin, and read the panel only. The user bot gets plugin menu buttons (px:<id>:<data>) and /commands, published in its command menu. transformSubscription rewrites served links and profiles, cached by input and checked to keep their shape. Example channel-gate; author docs and types updated.
The panel lists a community catalog: an index signed with ed25519 by a key built into the panel, every package pinned by its sha256. Installing or updating from it goes through the usual consent screen; a version the maintainers reviewed is marked. The operator is told once about each new version of an installed plugin and nothing updates by itself. A mirror is a copy of the folder, set in the UI. Maintainers build and sign the index with `rospanel plugin keygen` and `rospanel plugin index`. Package downloads by link are no longer capped at the 4 MB a plugin's fetch gets.
A theme plugin restyles the subscription page with theme/theme.css and the images and fonts next to it, served by the panel. The page itself stays the panel's, so payments, devices and Telegram keep working. Anything loaded from elsewhere is refused, and the files are served inert (nosniff, CSP sandbox). panel.blob keeps data too big for a plugin's memory in temporary files for one call: from text, from a /v1 answer or a fetched one, sent as a request body or as a file in a multipart form, hashed or read back up to 4 MB. Blobs are left out of backups and cleared at start. Examples theme-night and users-csv.
…tension points, API, testing, publishing, troubleshooting (RU/EN)
The catalog page, its endpoints, the update notices, internal/plugin/catalog, the keygen/index commands and migration 0116 are removed; plugins install from a zip or a link. The code stays in 624a331 for when it comes back.
A malformed widget can no longer take the dashboard down (shape checked on the server and in the page). View-only admins can open logs, code and settings; a paused or failed plugin can be switched off or started again; rollback asks first; installing an installed id updates it; select and switch settings send the value shown; the list, code and log dialogs stop loading on errors; plugin buttons refresh after updates, rollbacks and removals and refresh the screen they act on; settings and action errors get their own wording.
Security: an admin can grant a plugin only permissions they hold; the SQL guard follows SQLite's own trigger rule (keywords used as names no longer hide a PRAGMA), one value is capped at 8 MB and the quota is reapplied after every call; theme CSS refuses escapes, image-set and addresses, and the subscription page sends a CSP keeping images and fonts on the panel. Stability: every call waits for a busy plugin no longer than it may run, a plugin calling itself through the panel is refused instead of deadlocking, a caller that disconnects no longer kills the VM or counts against the breaker, backups no longer wait forever on a busy plugin, and the bot answers plugin presses and commands off its update loop. Plugin events lease one row per idle plugin, so a busy plugin no longer floods SQLite with put-backs; the storm breaker needs the plugin's own writes too, so the panel's bulk events no longer pause it; events raised while plugins start are queued. Data: rollback restores the snapshot of the update it undoes; a paused plugin stays paused across restarts and its payment orders are not cancelled by age; a quota pause during start keeps the plugin paused; uninstalling stops queued calls. Prices: a plugin's discount shows on the page and in the bot, and is quoted before the plan lock. Device refusals are kept per HWID, failures are not cached for the bot menu and page blocks, sign-up checks carry ref and source.
- each extension point needs the permission covering what it is handed (events, hooks, channel, card fields, sub blocks, bot: users.view; money events: billing.view; price: billing.manage; subscription: users.manage; payment: payments.manage); examples updated - buttons, widgets and card fields only for admins holding every permission the plugin was granted; rollback needs the previous version's permissions - the breaker keeps what waits for the plugin and retries it itself (5 min, 15 min, hourly; 3 failures after a retry pause it again); a plugin that failed to start at boot is retried the same way; events survive a restart - calls into a plugin are served in arrival order (a channel gate) - an action queued behind an uninstall no longer brings the plugin back - the plugins page reads a published copy and never waits on a long call - quota: paused only when a write was refused and the database is still full; WAL trimmed to 8 MB after checkpoints - update snapshots before switching (written aside, synced, renamed); rollback refuses without the snapshot; half-written files swept at start - SQL guard reads $a(...) parameters as SQLite does - fetch errors name only scheme://host, never the path or query - bool settings are always "true"/"false" - theme CSS: escapes decoded before the checks, inline data: SVG allowed - a surface that throws is left alone for 30 s; plugin log lines copied into the panel's journal capped at 20 a minute; 4 event workers - migration 0116: own indexes for plugin and endpoint deliveries
…extension points, dark theme focus rings and drop zone use the theme's tint
…aker, quota, fetch ports, update snapshots, what tests do not check
…dbox tests and trial runs - drafts (migration 0117, files encrypted like plugin settings) and /api/plugin-drafts: create from the rule builder, the code template, an installed plugin or a zip; save; check; run test.js; a trial run of an event or an export; download the package or the sources; hand the package to the ordinary consent screen - internal/plugin/builder: when an event (or a schedule) and conditions hold, Telegram, Discord, HTTP, extend, enable/disable, tag/untag, log — compiled to a plugin whose main.js is a fixed interpreter with the rules as JSON, granted exactly the permissions and hosts the rules need, each action once per event - sandbox: the author tools' harness on the panel's compiled engine, one run at a time, 60 s tests / 30 s runs; a trial run's panel.api reads only, with the permissions both the draft and the admin hold - devkit works on files in memory (TemplateFiles, PackFiles, ZipFiles, RunTestFiles); manifest.Unpack for sources
…ded on open), tests and trial runs, install and download
…en rows in the editor An action taken out of the rules took none of its code with it — every helper stayed in main.js and read as code piling up. main.js is now put together from the parts runtime.js is split into, by what the rules have; every kind of action, on an event or a schedule, with conditions or without, is checked to run. The condition row had three heights and the switches stretched across the page.
…the chat in the plugin's settings A group or channel is where the plugin is installed, like the bot token: it is a setting (telegram_chat) the operator fills in after install, not part of the rule. "To the user" needs nothing; a scheduled rule has no user to write to.
…the plugin's settings No choice of recipient in the rule: one chat ID setting beside the bot token, whatever chat it is.
… summary, why a run broke, and the console folded Also: a setting the rules no longer use leaves the trial settings (a stale discord_webhook stopped every test), the sandbox skips a dev.config key the plugin has no setting for, and a test's stack names test.js instead of main.js.
Each setting is a row with its hint; a stored secret is marked set, a required one still empty is marked, Save waits for a change. The card keeps in words what asks to be done (start again, fill in the settings) and the plugin's own buttons; the tools — settings, log, code, editor, update, rollback, remove — are icons.
- panel.t fills placeholders in one pass and bounds the result as it writes: a value carrying other placeholders could multiply into gigabytes and end the panel, not the plugin. - A plugin's migrations have their own MigrateTimeout: a statement that never ends is interrupted and rolled back instead of wedging every plugin at boot. - The theme CSS check lowers ASCII only: a Unicode lowering shifted byte offsets (the Kelvin sign), letting an outside url() through or panicking. Invalid UTF-8 is refused. - SetConfig takes a clear list (a secret can be emptied), and refuses to empty a required setting of an enabled plugin (SetupError, also from Enable). - Calls with a short budget (decisions, the subscription, the bot's menu) do not wait for main.js to load again: let through as busy, reloaded meanwhile. - Probe loads a package's code as a start would, for the editor's check. - panel.time.offset gives the panel's timezone to plugins. - onHttp serves every JavaScript MIME type as text (jscript, livescript too). - A storm seen as the operator switches a plugin off no longer switches it back on; uninstall drops the record before stopping, so a failed delete leaves the plugin running as it was.
- internal/plugin/builder/events.go describes each panel event's payload: the
user's fields, the event's own (types, examples, which are not always there),
where its user comes from, whether actions on the user can run, and a sample.
A test keeps it complete and every description in both dictionaries.
- user.* now works on plan.*, balance.adjusted, referral.reward and promo.*
(their data is the user); the default HTTP body is the whole event, id and
created_at included; ctx has event_id and created_at.
- Placeholders take formats: {{x|date}}, |datetime (panel timezone), |days,
|gb, |rub; an unknown one is refused.
- Validation refuses rules that feed themselves (extend on limits_changed, an
enable/disable pair), actions on user.deleted, hosts the manifest would
refuse, more hosts than net holds, Discord texts over 2000 characters. Tags
are stored normalized, untag writes only on a change, a rejected sign-up with
no account skips the user's actions, gt/lt on a missing field never match.
- The generated smoke test uses each event's sample.
- Examples require panel >=5.0.0.
… safer editor saves - Plugins and the editor are one list: "Add" installs a package or creates one (builder or code); a draft not installed shows with a gray dot, an installed plugin with an orange/red dot whose hint says what is wrong (settings to fill in, paused, failed, changes not applied). - One draft per plugin, held by a unique index (migration 0118, duplicates merged to the last edited); drafts carry rev, so a save from an older read (a version bump racing an autosave) is refused, not written over. Applying over the installed version bumps the patch version. - A builder draft whose rules have problems is not checked, tested, run, packaged or installed from the files of its last good rules. Check loads the code (Probe) in the sandbox slot. The sources download blanks secret settings. - Settings: a cross clears a value; a stored secret is marked to go, undone before saving. Removing and rolling back a plugin ask for the password. - The editor: an "Event data" panel per rule lists every field with its description and example (click to insert, |date/|gb/|rub buttons, a sample event); actions on a user hidden where none can run; one save in flight at a time, a failed save kept and retried, close asks when edits are unsaved, Back sends what waits; per-file undo; a phone header. - A failed install keeps the upload; the oldest upload is evicted first; the outbox drop for a plugin uses its partial index.
…fixes govulncheck reports GO-2026-6611/6612/6613/6617 as reachable on go1.26.6 and x/net v0.59.0 (handlePluginHTTP's http.Error, xray.EnsureBinary's Get).
… 1.26.9 - jsvm: a string is checked against maxGuestString before len(s)+1 is computed and allocated (CodeQL: size computation may overflow). - h2fix: Go 1.26.9 holds a plaintext HTTP/2 response past ReadHeaderTimeout by itself, so the unwrapped case now expects it held; the wrapper stays until it is taken out on its own.
Shu1t3
added a commit
to Shu1t3/rospanel-shu1t3
that referenced
this pull request
Oct 12, 2026
Merge AppsGanin/rospanel main at 4cb8724 (v5.0.1). Accept the Community Plugins subsystem (AppsGanin#108): - Sandboxed WASM/QuickJS runtime (wazero), extension points, lifecycle and cron triggers - Host APIs, plugin-declared payment methods, dynamic in-panel widgets and actions - In-panel Plugin Studio editor with visual rule builder, CodeMirror editor, tests and trial runs - Plugin drafts management, import/export and packaging - Upstream migrations 0112-0118 and outbox integration - Maphash-based transform cache keys and editor sources zip handling Preserve fork safeguards: - Fork module namespace (github.com/Shu1t3/rospanel-shu1t3) and dependency versions - Go 1.27.2 in go.mod and Dockerfile - Standby / fencing checks for background tasks and migration state management - Happ subscriptions routes and models - Fork release version 4.3.0, changelog, and Russian documentation - Invalidate settings cache and integrated plugin checkpointing Validation: - npm build and biome lint passed with 0 errors - go build ./... passed - go vet ./... passed - go test for internal/plugin/..., internal/core/..., internal/store/..., internal/telegram/..., and cmd/rospanel passed cleanly
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.
Community plugins for RosPanel: an operator installs a zip (or writes one in the panel), consents to what it asks for, and it runs sandboxed with its own database. Author docs are in
docs/plugins(RU/EN), examples inexamples/plugins.What a plugin can do
onEvent), on a schedule (cron), and a delivery channel for users the bot cannot reach.beforeSignup,beforeDeviceBind, price quotes; they fail open and never hold a sign-up longer than 300 ms.onHttp) behind the callback secret.panel.apiwith exactly the permissions it was granted,fetchto its declared hosts only.How it runs
Editor in the panel
Plugins and drafts are one list. Add installs a package or starts a new plugin:
|date,|gb,|rubformats. Rules that would feed themselves, or act on a user who is not there, are refused.Database
Migrations
0112–0118, all additive: plugins, the outbox for plugin deliveries, alert and role permission bits, drafts.0118keeps one draft per plugin (removes duplicates, keeping the last edited) and adds a revision counter for safe concurrent saves.Checks
go test -race ./...on Linux (Docker, CI's Go) — green; golangci-lint v2.12.2 — 0 issues; webtsc, biome, build — clean.Before releasing
The examples require
panel >=5.0.0. ARelease-As: 5.0.0footer must reachmainat release time, or release-please cuts 4.5.0 and the examples will not install on it.