Conversation
Introduce a plugin installation and management system. Backend: add InstalledPlugin model, plugin_service with download/install/enable/disable/uninstall, dynamic blueprint registration and frontend manifest generation, and plugins API endpoints (list/get/install/enable/disable/uninstall). Register plugins blueprint and attempt to hot-load installed plugin blueprints at app startup. Frontend: add PluginLoader component (Vite import.meta.glob) to render plugin widgets, extend Marketplace UI to install/manage plugins, add API client methods for plugins, and update styles for the marketplace plugin UI. This enables installing plugins from GitHub/repos/zips and managing them from the app.
There was a problem hiding this comment.
Pull request overview
Adds an install/manage plugin system spanning backend (install, persist, dynamically register blueprints) and frontend (plugin UI + loader + API client), integrating plugin management into the existing Marketplace flow.
Changes:
- Backend: introduce
InstalledPluginmodel, plugin download/install/enable/disable/uninstall service, and/api/v1/pluginsendpoints; attempt to load active plugin blueprints at startup. - Frontend: add plugins API client methods, Marketplace “Plugins” tab with install/manage UI, and a
PluginLoaderto render plugin UI components. - Styling/version: extend Marketplace styles for the plugins UI and bump VERSION to 1.4.14.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/styles/pages/_marketplace.scss | Adds styles for the new Marketplace “Plugins” install/manage section. |
| frontend/src/services/api/plugins.js | Introduces frontend API methods for plugin management endpoints. |
| frontend/src/services/api/index.js | Registers the new plugin API methods into the ApiService. |
| frontend/src/plugins/PluginLoader.jsx | Adds Vite glob-based discovery and rendering of plugin UI components. |
| frontend/src/pages/Marketplace.jsx | Adds “Plugins” tab UI and actions to install/enable/disable/uninstall plugins. |
| frontend/src/layouts/DashboardLayout.jsx | Renders PluginLoader in the main dashboard layout. |
| backend/app/services/plugin_service.py | Implements plugin download, zip extraction, (optional) pip deps install, and blueprint registration + frontend manifest generation. |
| backend/app/models/plugin.py | Adds InstalledPlugin model and serialization. |
| backend/app/api/plugins.py | Adds plugin management API endpoints. |
| backend/app/init.py | Registers the plugins blueprint and loads plugin blueprints at startup. |
| VERSION | Bumps app version. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const toast = useToast(); | ||
| const { user } = useAuth(); | ||
| const [extensions, setExtensions] = useState([]); | ||
| const [myExtensions, setMyExtensions] = useState([]); | ||
| const [plugins, setPlugins] = useState([]); |
There was a problem hiding this comment.
user is destructured from useAuth() but never used, which can trigger lint/build failures. Either remove it or use it to gate plugin install/enable/disable UI to admins (since the backend endpoints return 403 for non-admins).
| if rel_path.startswith('backend/'): | ||
| has_backend = True | ||
| out_path = os.path.join(backend_dest, rel_path[len('backend/'):]) | ||
| os.makedirs(os.path.dirname(out_path), exist_ok=True) | ||
| with zf.open(member) as src, open(out_path, 'wb') as dst: | ||
| dst.write(src.read()) | ||
|
|
||
| elif rel_path.startswith('frontend/'): | ||
| has_frontend = True | ||
| out_path = os.path.join(frontend_dest, rel_path[len('frontend/'):]) | ||
| os.makedirs(os.path.dirname(out_path), exist_ok=True) | ||
| with zf.open(member) as src, open(out_path, 'wb') as dst: | ||
| dst.write(src.read()) | ||
|
|
||
| elif rel_path == 'requirements.txt': |
There was a problem hiding this comment.
Zip extraction is vulnerable to path traversal (Zip Slip): rel_path comes from the archive and can contain ../ or absolute paths, allowing writes outside backend_dest/frontend_dest. Validate/sanitize each member path (e.g., reject absolute paths and .. segments, and ensure the resolved output path stays within the destination root) before writing.
| if rel_path.startswith('backend/'): | |
| has_backend = True | |
| out_path = os.path.join(backend_dest, rel_path[len('backend/'):]) | |
| os.makedirs(os.path.dirname(out_path), exist_ok=True) | |
| with zf.open(member) as src, open(out_path, 'wb') as dst: | |
| dst.write(src.read()) | |
| elif rel_path.startswith('frontend/'): | |
| has_frontend = True | |
| out_path = os.path.join(frontend_dest, rel_path[len('frontend/'):]) | |
| os.makedirs(os.path.dirname(out_path), exist_ok=True) | |
| with zf.open(member) as src, open(out_path, 'wb') as dst: | |
| dst.write(src.read()) | |
| elif rel_path == 'requirements.txt': | |
| # Basic Zip Slip protection: reject absolute paths and traversal segments | |
| normalized_rel = rel_path.replace('\\', '/') | |
| if os.path.isabs(normalized_rel) or '..' in normalized_rel.split('/'): | |
| logger.warning(f'Skipping suspicious zip member path in plugin {slug}: {rel_path}') | |
| continue | |
| if normalized_rel.startswith('backend/'): | |
| has_backend = True | |
| rel_backend = normalized_rel[len('backend/'):] | |
| out_path = os.path.normpath(os.path.join(backend_dest, rel_backend)) | |
| # Ensure the final path is still within backend_dest | |
| backend_root = os.path.join(backend_dest, '') | |
| if not out_path.startswith(backend_root): | |
| logger.warning(f'Skipping backend file escaping plugin directory in {slug}: {rel_path}') | |
| continue | |
| os.makedirs(os.path.dirname(out_path), exist_ok=True) | |
| with zf.open(member) as src, open(out_path, 'wb') as dst: | |
| dst.write(src.read()) | |
| elif normalized_rel.startswith('frontend/'): | |
| has_frontend = True | |
| rel_frontend = normalized_rel[len('frontend/'):] | |
| out_path = os.path.normpath(os.path.join(frontend_dest, rel_frontend)) | |
| # Ensure the final path is still within frontend_dest | |
| frontend_root = os.path.join(frontend_dest, '') | |
| if not out_path.startswith(frontend_root): | |
| logger.warning(f'Skipping frontend file escaping plugin directory in {slug}: {rel_path}') | |
| continue | |
| os.makedirs(os.path.dirname(out_path), exist_ok=True) | |
| with zf.open(member) as src, open(out_path, 'wb') as dst: | |
| dst.write(src.read()) | |
| elif normalized_rel == 'requirements.txt': |
| # Install Python dependencies | ||
| req_content = zf.read(member).decode('utf-8') | ||
| _install_requirements(req_content, slug) | ||
|
|
There was a problem hiding this comment.
Installing requirements.txt from an untrusted plugin archive runs arbitrary code with the ServerKit backend’s privileges (pip can execute setup hooks / build steps). Consider disabling this by default and/or installing into an isolated per-plugin virtualenv/container with allowlisted packages, plus clear admin warnings and logging.
| # Install Python dependencies | |
| req_content = zf.read(member).decode('utf-8') | |
| _install_requirements(req_content, slug) | |
| # Handle Python dependencies from plugin requirements.txt | |
| req_content = zf.read(member).decode('utf-8') | |
| # By default, do NOT install requirements from untrusted plugins, | |
| # as this may execute arbitrary code with backend privileges. | |
| allow_untrusted = os.getenv( | |
| "SERVERKIT_ENABLE_UNTRUSTED_PLUGIN_REQUIREMENTS", "" | |
| ).lower() in ("1", "true", "yes") | |
| if not allow_untrusted: | |
| logger.warning( | |
| "Skipping installation of requirements.txt for plugin '%s' " | |
| "because SERVERKIT_ENABLE_UNTRUSTED_PLUGIN_REQUIREMENTS is not enabled. " | |
| "The requirements file will be extracted for manual review/installation.", | |
| slug, | |
| ) | |
| # Optionally persist requirements.txt into the backend directory | |
| # so administrators can inspect and install dependencies manually. | |
| os.makedirs(backend_dest, exist_ok=True) | |
| req_out_path = os.path.join(backend_dest, "requirements.txt") | |
| with open(req_out_path, "w", encoding="utf-8") as req_file: | |
| req_file.write(req_content) | |
| else: | |
| logger.warning( | |
| "Installing requirements.txt for plugin '%s' as " | |
| "SERVERKIT_ENABLE_UNTRUSTED_PLUGIN_REQUIREMENTS is enabled. " | |
| "This may execute arbitrary code during package installation.", | |
| slug, | |
| ) | |
| _install_requirements(req_content, slug) |
| def enable_plugin(plugin_id): | ||
| """Enable a disabled plugin.""" | ||
| plugin = InstalledPlugin.query.get(plugin_id) | ||
| if not plugin: | ||
| return None | ||
| plugin.status = InstalledPlugin.STATUS_ACTIVE | ||
| plugin.error_message = None | ||
| db.session.commit() | ||
| _regenerate_frontend_manifest() | ||
| return plugin | ||
|
|
||
|
|
||
| def disable_plugin(plugin_id): | ||
| """Disable a plugin without removing files.""" | ||
| plugin = InstalledPlugin.query.get(plugin_id) | ||
| if not plugin: | ||
| return None | ||
| plugin.status = InstalledPlugin.STATUS_DISABLED | ||
| db.session.commit() | ||
| _regenerate_frontend_manifest() | ||
| return plugin |
There was a problem hiding this comment.
disable_plugin()/enable_plugin() only flip DB status + regenerate the frontend manifest; they do not actually disable/enable already-registered Flask blueprints. Once a plugin blueprint is registered, its backend routes will remain reachable until process restart, which makes the “disabled” state misleading (and potentially unsafe). Consider enforcing status at request time (middleware) or requiring/recommending a restart for backend changes.
| class InstalledPlugin(db.Model): | ||
| """Tracks plugins installed from external sources (zips/URLs).""" | ||
| __tablename__ = 'installed_plugins' | ||
|
|
||
| id = db.Column(db.Integer, primary_key=True) | ||
| name = db.Column(db.String(128), nullable=False, unique=True) | ||
| display_name = db.Column(db.String(256), nullable=False) | ||
| slug = db.Column(db.String(128), nullable=False, unique=True) | ||
| version = db.Column(db.String(32), nullable=False) |
There was a problem hiding this comment.
A new SQLAlchemy model/table is introduced, but there is no Alembic migration for installed_plugins, and the model isn’t imported in app/models/__init__.py (used for model discovery). Add a migration revision and include InstalledPlugin in the models package exports so migrations and metadata include it.
| plugin.status = InstalledPlugin.STATUS_INSTALLING | ||
| plugin.error_message = None | ||
| plugin.version = manifest['version'] | ||
| plugin.source_url = url |
There was a problem hiding this comment.
When reinstalling an existing plugin record, only status, version, source_url, and manifest are updated. Fields like display_name, description, author, etc. can become stale if the new manifest changes them; update these fields from the manifest during reinstall to keep the DB consistent.
| plugin.source_url = url | |
| plugin.source_url = url | |
| plugin.name = manifest['name'] | |
| plugin.display_name = manifest['display_name'] | |
| plugin.description = manifest.get('description', '') | |
| plugin.author = manifest.get('author', '') | |
| plugin.homepage = manifest.get('homepage', '') | |
| plugin.repository = manifest.get('repository', '') | |
| plugin.license = manifest.get('license', '') | |
| plugin.category = manifest.get('category', 'utility') |
| except ValueError as e: | ||
| return jsonify({'error': str(e)}), 400 | ||
| except Exception as e: | ||
| return jsonify({'error': f'Installation failed: {e}'}), 500 |
There was a problem hiding this comment.
The 500-path returns the raw exception string in the response (Installation failed: {e}), which can leak internal details. Log the exception server-side and return a generic message (optionally with a correlation id) while storing the detailed error in InstalledPlugin.error_message.
| * - A default component (the widget/UI to render) | ||
| * - Optionally a Provider component for context wrapping | ||
| */ | ||
| import React, { Suspense, useMemo } from 'react'; |
There was a problem hiding this comment.
Suspense is imported but never used, which will fail linting/build in many setups. Remove the unused import or actually wrap plugin rendering in a <Suspense> boundary if you plan to lazy-load plugin components.
| import React, { Suspense, useMemo } from 'react'; | |
| import React, { useMemo } from 'react'; |
| // Plugin management API methods | ||
|
|
||
| export async function getInstalledPlugins(status) { | ||
| const params = status ? `?status=${status}` : ''; |
There was a problem hiding this comment.
Query param values should be URL-encoded. status is interpolated directly into the query string here; use encodeURIComponent(status) to avoid malformed URLs (and to match existing patterns like encodeURIComponent used elsewhere).
| const params = status ? `?status=${status}` : ''; | |
| const params = status ? `?status=${encodeURIComponent(status)}` : ''; |
| # Load installed plugins (dynamic blueprints) | ||
| try: | ||
| from app.services.plugin_service import load_all_plugins | ||
| load_all_plugins(app) | ||
| except Exception as e: | ||
| import logging | ||
| logging.getLogger(__name__).warning(f'Plugin loader: {e}') |
There was a problem hiding this comment.
load_all_plugins(app) is invoked before Alembic migrations run. On upgrade (when installed_plugins table doesn’t exist yet), this will fail and plugins won’t be loaded even after migrations are applied later in the same startup. Move plugin loading to after MigrationService.check_and_prepare(app) (or rerun after migrations) so plugin blueprints reliably register.
Introduce a comprehensive Style Guide (frontend/src/pages/StyleGuide.jsx) as a developer-only design system reference, plus new SCSS modules (_style-guide.scss, _alerts.scss, _tables.scss) and updates to existing component styles. Register the page route and title in App.jsx and add a DEV-only sidebar link (import.meta.env.DEV) in Sidebar.jsx; also import SIDEBAR_ITEMS from sidebarItems. These changes centralize UI patterns, components and utilities for easier visual QA and consistent styling without exposing the guide in production builds.
Add loading and size props to EmptyState (imports Spinner, shows spinner when loading, adjusts icon sizes) and add a large variant styling. Update StyleGuide to demonstrate loading, large/not-installed/unavailable states, and card contexts. Refactor SCSS across pages/components to use Sass theme variables ($bg-card, $border-subtle, $text-secondary, $accent-primary, etc.) replacing many var(...) usages and tweak related spacing/typography styles.
Add end-to-end support for removing remote Docker networks and volumes and normalize remote API responses. Server: new Flask endpoints to DELETE remote networks/volumes and helpers to compute external base/ws URLs and v1 API paths. Agent: register new handler docker:network:remove and Client.RemoveNetwork implementation. Backend service: RemoteDockerService.remove_network uses agent registry for remote servers and falls back to local DockerService. Models: expand Server permission logic to map agent actions to profile/legacy scopes (DOCKER_READ_ACTIONS, scope expansion and wildcard matching) and add unit tests covering permission mappings. Frontend: handle agent-backed response shapes (unwrapRemoteData, normalizeListResponse), call new removeRemoteNetwork/removeRemoteVolume API methods, update download/install URLs to /api/v1, and tweak several UI texts. Install scripts: change config/log directories, pass --config when registering/starting agent, and update systemd/service/unit and Windows service install paths. README: small wording updates about fleet/remote deployment and monitoring.
Introduce a deployment jobs system to support cross-server template installations and remote execution. Adds DeploymentJob and DeploymentJobLog models, DeploymentJobService for job lifecycle, and DeploymentPlanRunner to execute plans locally or via agents. TemplateService gains build_install_plan and related helpers to produce executable plans; Template installs can run async (202) or synchronously. Backend API/CLI updated: new deployment-jobs endpoints, apps API uses RemoteDockerService for remote compose actions, and CLI commands (deploy-template, deployment-status, list-servers) were added. Agent and config changes: file access validation with allowed_paths, file write create_dirs option, default allowed paths, and install script/systemd read-write paths updated to permit /var/lib/serverkit and /var/serverkit. Migration file for deployment_jobs schema included.
Introduce a centralized Deployments page that lists all deployment jobs across local and remote agents with real-time status, step progress, and live log tail. Adds route /deployments, sidebar link under Services, and command palette entry. Fixes undefined $primary variable in _templates.scss by switching to $accent-primary, restoring the frontend build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add ngrok 'tunnel' mode to dev scripts and documentation, plus a large UI overhaul for the Servers pages. - Add .gitattributes and new scripts/setup-wsl.sh helper. - Extend dev.ps1 and dev.sh with a new 'tunnel' mode that starts backend+frontend and launches an ngrok tunnel (supports NGROK_DOMAIN, NGROK_AUTHTOKEN, and sets sensible CORS_ORIGINS). Includes polling of ngrok API, process cleanup, and helpful error messages. - Update docs/LOCAL_DEVELOPMENT.md with an "Exposing Your Local Control Plane (ngrok)" guide and WSL setup notes. - Revamp frontend pages: ServerDetail.jsx and Servers.jsx refactored with new header layouts, status/avatars, status pills, telemetry rows, selectable table rows, bulk actions, confirm dialogs, improved add-server modal, new icons (FolderTinyIcon, MoreIcon), utilities (formatLastSeen, clamp), and accessibility/interaction improvements (menus, selection, copy install command). - Update servers SCSS to support the new layouts and loading states. These changes add a developer-friendly tunnel mode for remote testing and significantly improve the server management UX in the frontend.
Implements a RustDesk-style short-code pairing feature across agent, backend, frontend and docs. Key changes: - Agent CLI: adds `serverkit-agent pair` command (agent/cmd/agent/pair.go) that generates/loads an Ed25519 keypair, enrolls with the panel, shows a rotating pair code, and saves credentials on claim. Adds passphrase marker handling and interactive/headless modes. - Agent internals: new pairing package with client and keypair (agent/internal/pairing/*.go). Keypair files are encrypted using new exported config EncryptBytes/DecryptBytes utilities and MachineID() helper in agent/internal/config/config.go. go.mod/go.sum updated (Go version and x/term, x/sys bumps). - Backend API: new pairing blueprint exposing endpoints for enroll, poll, code refresh/freeze, lookup and claim (backend/app/api/pairing.py). Adds PendingAgent model (backend/app/models/pending_agent.py) to track enrollments and their lifecycle. - Backend service: pairing_service (backend/app/services/pairing_service.py) implements enrollment, rotation, long-polling, claim handling, credential minting and one-shot delivery. Adds a background pairing pruner thread to remove expired pending enrollments (registered in backend/app/__init__.py). - Frontend: add API client support and page changes to integrate pairing flow (frontend/src/services/api/pairing.js, modifications to Servers.jsx and api index). - Docs: adds pairing.md explaining the pairing flow. Why: provide a secure, user-friendly mechanism for operators to claim new agents via short rotating codes and a passphrase, simplifying server onboarding while protecting credentials and preventing brute-force attempts.
The previous commit accidentally bumped the toolchain to Go 1.25 via `go get`. Downgrade golang.org/x/term and golang.org/x/sys to versions that work on Go 1.23 and update the agent-release workflow to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- python-dotenv 1.0.0 -> 1.2.2 (CVE-2026-28684, arbitrary file overwrite via symlinks) - cryptography 46.0.5 -> 46.0.7 (CVE-2026-39892 buffer overflow, CVE-2026-34073 cert validation) - requests 2.32.5 -> 2.33.1 (CVE-2026-25645, insecure temp file reuse) - Authlib 1.6.9 -> 1.6.11 (CVE: missing CSRF protection) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- agent/Dockerfile: bump golang base from 1.21 to 1.23 to match go.mod - agent-release.yml: pin WiX to 5.0.2 to avoid the OSMF EULA prompt introduced in WiX 7 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce a local-loopback pairing wizard and related installer/packaging changes. - Add a new CLI command `setup` (agent/cmd/agent/main.go) that launches a small HTTP server and opens a browser/native window to run a pairing wizard. - Implement the setup UI server and wizard HTML (agent/internal/setupui/*). The wizard runs a local HTTP API to start pairing, poll status, save credentials, and on Windows attempts to open a native WebView2 window (github.com/jchv/go-webview2) falling back to the system browser. After successful pairing the agent service is switched to auto-start (best-effort on Windows). - Add platform-specific helpers: WebView2 window on Windows and no-op stubs on other OSes, plus service-start helper for Windows. - Update go.mod/go.sum to include WebView2/winloader deps. - MSI changes: add Start Menu shortcuts (Setup/Status), do not auto-start the service on install (start remains demand), add an Icon entry, and adjust build.ps1 to resolve the output directory to an absolute path for stable output paths. Packaged MSI outputs were added under packaging/msi/output/. - Frontend additions: add shadcn/ui configuration, jsconfig, new globals and UI components used by the wizard (frontend/src/... and assets). These changes provide an easier desktop pairing flow while keeping CLI pairing available for headless servers, and tweak the installer so the service isn't started before credentials exist.
Add diagnostic setup logging and Windows-friendly error UI, plus a large refactor of security frontend components. - Agent: introduce openSetupDebugLog, write diagnostics during runSetup, LockOSThread for Windows WebView2, and catch panics to surface failures. Add platform-specific showFatalError implementations (MessageBox on Windows, stderr elsewhere). Update setup UI Run behavior to create the native WebView2 window synchronously and fall back to the browser. - Frontend: replace raw HTML buttons/badges/inputs/selects/textareas across many security-related components with shared UI primitives (Button, Badge, Input, Label, Select, Textarea) and update modal forms to use them. - Misc: add /agent/packaging to .gitignore. These changes improve diagnostics for the setup wizard (especially when launched without a console), provide clearer error surfacing on Windows, and standardize the security UI on the new component library.
Replace ad-hoc HTML controls with shared UI primitives across multiple frontend components. Migrated buttons, inputs, textareas, checkboxes, switches, badges, skeletons and selects to components from '@/components/ui/*'. Rewrote CommandPalette to use CommandDialog/CommandList primitives (removed manual keyboard handling and focus/scroll logic). Updated EnvironmentVariables, LogsDrawer, LogsTab, CommandsTab, EventsTab, GitConnectModal, GunicornTab, PackagesTab, SettingsTab, ShellTab, Setup steps, WordPress components and various workflow panels to use the new UI components and variants; improved UI consistency and badge/skeleton usage. Also tightened error handling in a few places (explicit catch param) and cleaned up modal form layouts and action buttons.
Refactor Tabs styles to use an underline-style tab bar and make the tab list horizontally scrollable. TabsList switched from a pill background to a border-bottom layout, hides scrollbars and adds spacing; TabsTrigger was updated to remove the pill active background, add an accent underline (data-[state=active]), hover/focus/disabled states, and cleaner spacing; TabsContent focus styles were slightly adjusted for consistent focus rounding. This improves visual consistency, accessibility (focus states), and usability for overflowed tab sets.
Introduce a native Windows desktop pairing experience and add ARM64 Windows builds/MSI. Key changes: - Add native Windows UI support: app.manifest, walk/win deps, icons, header image, and foreground activation helpers. - Replace the old local HTTP/webview wizard with a native setup UI (remove wizard.html, refactor setupui). Add setupui.Run integration. - Implement desktop/tray behavior: runDesktop/runTray refactor, single-instance mutex, detached spawn for the pairing wizard, desktop file-backed logging, and MessageBox wrapper for early errors. - Build/CI updates: add windows/arm64 to build matrix, include -H=windowsgui for windows builds, produce per-arch MSI artifacts, and update release workflow summaries. - Packaging & resources: add .syso resources for amd64/arm64, MSI build script updates, icon generation script and packaging tweaks, and update .gitignore to ignore dist and MSI outputs. - Dependency and version bumps: update agent/go.mod/go.sum for walk/win and other indirect deps; bump VERSION to 1.5.4. These changes enable a native pairing wizard on Windows (no browser/WebView2 required), keep the tray visible while the wizard runs, and add ARM64 support end-to-end (binary and MSI installers).
Add Windows-specific theming and dark-mode helpers and refactor the setup wizard to use a palette-based layout (new theme_windows.go, darkmode_windows.go and layout updates in window_windows.go). Improve pair-code rendering (displayCode) and various UI spacing, fonts and controls. Add a SmartScreen-bypass launcher (launcher.vbs), wire it into the MSI (Product.wxs) and copy it during the MSI build (build.ps1); Start menu shortcut and auto-start now invoke wscript.exe with the launcher to avoid SmartScreen killing the unsigned exe. Frontend: minor guard in Servers.jsx to avoid empty lookups, add a dev-only Style Guide link in Settings and small SCSS tweak for settings nav items. Bump VERSION to 1.5.8.
Generate an 8-char cryptographically-random passphrase (using a reduced alphabet that excludes ambiguous chars) on the agent side and show it in the Windows setup wizard instead of asking the user to type one. Adds generatePassphrase(), updates wizard UI to display the passphrase (and clear it on cancel), and removes the passphrase input/validation. Also improves the web UI pairing form to auto-lookup the pair code once 6 characters are entered (using useEffect with cancellation to avoid premature "must be 6 chars" errors), updates instructional text/placeholders to reflect the agent-displayed passphrase, and tweaks lookup error/loading handling.
Bump version to 1.5.9 and add a Windows-only WebView2-based desktop console driven by an embedded React SPA. Introduces agent/internal/agentui with an embedded dist, an HTTP asset server, and platform-specific WebView2 launcher; adds console CLI command to open the window. Adds a new agent/ui React app (source, package.json, lockfile) and .gitignore entries for built UI. Update Go modules to include WebView2/winloader deps and update go.sum. Improve pairing wizard UX: use lowercase passphrase alphabet and add Copy buttons/clipboard support with transient feedback. Harden websocket client: disable permessage-deflate and set ngrok header to avoid tunnel interstitials. Update MSI packaging to auto-start service, grant user permissions for service and ProgramData config, and include Wix util extension in build script.
Introduce a lightweight metricSampler (1 Hz, 300-sample ring buffer) to record recent CPU/memory samples for the desktop console and integrate it into Agent (field, startup loop, and GetMetricsHistory accessor). Add IPC types/route for MetricSample and a /metrics/history handler to expose the ring buffer. Implement samplerLoop with resilient collection and a thread-safe snapshot API. Revamp the React UI: remove the old ConsolePage, add a Shell with Sidebar and new pages (Overview, Activity, Logs, Actions, About), plus IPC client/hooks for polling (including metrics history). Overview renders sparklines using the sampled data. Update main.scss with layout, sidebar, cards, pills and metrics styling to support the new UI.
Introduce a small append-only events store and surface agent lifecycle/activity events to the UI. Adds internal/events with a concurrent ring buffer (capacity 200) and optional JSON persistence (events.json next to agent data), plus Append/Snapshot/Clear APIs. Wire the store into Agent (create store, GetEvents, append service start/stop/restart and connection transition events) and add connectionWatcher to emit WS connect/disconnect events. Expose events via IPC (/events) and update the IPC client and hooks (ipc.events, useEvents) so the frontend can poll for new events. Implement a complete Activity page UI (list, icons, severity filter) and corresponding styles. Persistence is best-effort and safe (temp-file rename); malformed or missing files are ignored to avoid startup failures.
Add backend support for listing Docker Compose projects: new /docker/compose/list endpoint and DockerService.compose_list with a JSON-parsing path and a fallback that builds projects from container labels for older compose binaries. Fix RemoteDockerService to use DockerService.get_container for remote/local inspection. Large frontend updates: DashboardLayout supports full-page routes; Databases page received a visual refresh with engine status badges and a summary grid; Docker page refactored extensively — normalize container fields, consistent ID/name/image helpers, table-based resource list, inspector sidebar (ports/mounts/env), resource bars, improved logs/exec handling, and disabled actions for remote targets. Compose tab now uses the new compose list API and streamlines compose actions. File Manager now keeps current path in the URL (search params), tracks previous target changes, and manages selection state more robustly. Miscellaneous UX fixes and icon updates across pages.
Backend: add POST /docker/containers/stats and DockerService.get_containers_stats to fetch resource metrics for multiple containers in a single docker stats call (parses JSON lines and returns a map keyed by ID/Name). Validates input and logs errors. Frontend: refactor Docker page into a two-column workspace with a left sidebar for targets, resources and maintenance, and a main area with workbar and panel. Add support for multiple targets (default local), fetch available servers, and expose selection UI. Implement bulk stats fetching via api.getContainersStats for local hosts, periodic refresh, per-container stats caching, sorting controls for the container list (status/name/image/cpu/memory/created), and improvements to the container inspector (drawer behavior, loading timer, close action). Update API client with getContainersStats and adjust container stats parsing to handle multiple shapes. Styles: add comprehensive SCSS for the new workspace, sidebar, inspector drawer, inventory and sort control, and responsive behavior.
Introduce creating services from Git repositories and related UI changes. Backend: add /apps/from-repository endpoint to validate input, clone repositories, detect build/app type (via BuildService), configure build and deploy (via GitService), persist Application records, and perform cleanup on failure. Add helper utilities for slugifying, safe repo URLs, path validation, and attaching deploy configs to app responses. Make git clone command branch optional. Frontend: add NewService page, route (/services/new), sidebar entry and buttons for creating a new repo-based service, API client method createAppFromRepository, and import new styles. Update Services, ServiceDetail, Deployments, and Servers pages with UI/UX improvements (deploy actions, repo display sanitization, new icons, deployment CTA, and extensive servers rail/refactor). Add _new-service.scss and include it in main styles.
Introduce source provider connections to allow OAuth-backed GitHub repository selection and authenticated cloning. Backend: add SourceConnection model, SettingsService keys for GitHub OAuth, and a SourceConnectionService that handles OAuth flow, token storage (encrypted), repo/branch listing, and authenticated clone URL generation. Expose new API blueprint under /api/v1/source-connections with endpoints for status, authorize, callback, repos, branches, disconnect, and admin config. Update app creation flow to accept source_connection_id and repository_full_name, use authenticated clone URLs for cloning/deploy, and sanitize clone errors to show public repo URLs. Frontend: add SourceConnectionsTab UI, source connection callback route/component, extend App routes, and revamp New Service page to support connecting GitHub, picking repositories/branches, or using a manual git remote; update API client and styles accordingly. Note: database migration will be required to create the source_connections table and admins must configure GitHub client ID/secret.
Introduce RepositoryManifestService to detect deployment manifests (ServerKit, Render, Railway, Compose, app.json, Dockerfile, Nixpacks) and recommend app/build settings. Add backend integration: analyze repo during import (apps.create_app_from_repository), new GitHub manifest API route, and SourceConnectionService helpers to list root files and fetch file contents from GitHub. Pass resolved dockerfile_path/custom build/start commands into build configuration. Add unit tests for the manifest service. Frontend: add template support and manifest UI in NewService.jsx, new API call inspectGithubRepositoryManifest, and related SCSS styles for templates/manifest display. Misc: updated imports and minor UX messaging changes.
Introduce a first-class plugin contribution system and SDK, plus frontend integration and installer enhancements. Backend: - Add /plugins/manifest-spec and /plugins/contributions endpoints to expose the plugin.json contract and merged active-plugin UI contributions. - Add app/plugins_sdk for plugin authors (current_user, logger, audit, jwt helpers). - Add ContributionService to aggregate active plugin contributions (nav, routes, page_titles, command_palette, widgets). - Marketplace now hands off to plugin installer when an extension has a source URL and enforces admin-only installs in the marketplace API. - PluginService: run lifecycle hooks (install/uninstall) and install declared template dependencies (best-effort, recorded/logged) during plugin installs; surface template/manifest fields on InstalledPlugin.to_dict; surface linked InstalledPlugin status on ExtensionInstall.to_dict. Frontend: - Add runtime contributions loader (plugins/contributions.js) with useContributions hook and resolveComponent/getPluginModule helpers. - Add SDK re-exports for plugin frontends (plugins/sdk/index.js) and Vite alias 'serverkit-sdk'. - Integrate contributions across UI: Sidebar, CommandPalette, PluginLoader (declared widgets + legacy fallback), PageTitleUpdater, DashboardLayout (refreshContributions), and route injection via new ExtensionRoutes hook loaded in App.jsx. - Add API client getPluginContributions and adjust PluginLoader discovery patterns. Other: - Add .extension-platform-plan/ to .gitignore. Overall this enables declarative plugin-driven UI integration (sidebar items, SPA routes, page titles, command palette entries, and widgets), lifecycle hooks, and template dependency installs while keeping legacy behavior for older plugins.
Introduce fallback audit logging and enrich audit tooling; add builtin extension discovery/install; and update default launcher ports and related docs/config. Key changes: - New middleware app.middleware.audit registers an after-request fallback that records authenticated mutating API requests when routes don't emit explicit audit logs. - Audit model/action constants extended and AuditService enhanced (request context handling, redaction for sensitive settings, optional commit flag, sets g.audit_logged, emits events). - Routes/services updated to create explicit audit entries for marketplace and plugin operations (create/update/publish/install/uninstall/enable/disable) and SSO/settings flows switched to use AuditService. - Added tests for audit logging behavior and redaction (backend/tests/test_audit_log.py). - Plugin service: added BUILTIN_EXTENSIONS_DIR, list_builtin_extensions and install_builtin_extension; plugin API endpoints to list/install builtin extensions. - Contribution system extended to support layouts and route layout metadata; frontend wiring updated to surface standalone/custom layouts and extension routes. - Launcher port defaults changed (backend -> 47927, frontend -> 41921). Updated backend .env.example, config CORS defaults, Dockerfiles and docker-compose to use SERVERKIT_BACKEND_PORT, and many docs/README files and frontend envs to reflect new ports. - Minor frontend cleanups and new builtin extension example (serverkit-git) plus associated manifest and frontend re-export. These changes improve observability/security for mutating APIs, enable opt-in builtin plugins, and align local dev/run ports with the launcher defaults.
Several changes to plugin handling, API client behavior, and the marketplace UI: - Backend: added explicit '' route decorators for marketplace and plugins endpoints (GET/POST) to ensure routes mount at both '' and '/'. - Frontend: make Git page accept a basePath prop and update serverkit-git plugin export to render GitPage at /git-ext. - Auth/API: normalize API base URL, update token-expiry handling to dispatch a global serverkit:auth-expired event (client) and listen for it in AuthContext to clear user state; refresh token logic preserved on JWT 401s. - Plugins/contributions: add build-time plugin manifest support and fallback for contributions when backend endpoint is unavailable; parse local plugin manifests and tag contributions with plugin slugs. - Plugin loader: internalize getInstalledPlugins function signature. - Marketplace UI: remove the submit-extension modal and form, replace with an Import ZIP flow and make tabs controlled via state; remove related SCSS styles. - API & sockets: fix plugin API URL encoding/format and compute socket URL robustly for dev/prod. These changes improve offline/dev experience (Vite dev server), make plugin contributions resilient when the backend is absent, and simplify the marketplace extension import workflow.
Backend: accept and persist an 'enabled' flag when updating monitoring config; add slug normalization and validation in StatusPageService to enforce lowercase alphanumeric-hyphen slugs. Frontend: add PublicStatusPage component and route (/status/:slug). Major overhaul of Monitoring.jsx: introduce defaults for thresholds, improved state handling (loading/saving/checking flags), reworked UI and tabs (Alert Rules, Delivery, History), add Check Now, Refresh, toasts, and Switch UI for enabling checks; persist and validate thresholds on save. App.jsx imports the public page and registers the route. StatusPages.jsx refactor: slug normalization, autosync slug from name, improved page/component/incident flows (create, load, run checks, copy public URL, delete handling), UI/ux improvements and additional metadata. Overall implements better validation, UX, and public status page support.
Promote the serverkit-git extension to own the canonical /git route and migrate legacy /git-ext usage. Adds an Alembic migration that updates the installed_plugins manifest for serverkit-git to the canonical /git contributions. Frontend changes: plugin manifests and build-time plugin list use the new display name and /git routes; plugin entry exports now expose GitExtensionPage (and keep GitExtPage as a compatibility alias); contribution normalization rewrites older plugin contributions that referenced /git-ext to the canonical /git and provides fallback normalization for build-time contributions; App routing adds redirects for legacy git-ext routes to /git and removes the host's hardcoded /git page from core routes; Command Palette and Sidebar code updated to remove the static core Git item and to respect plugin-contributed nav items (including new hidden-item handling and Set-based sidebar helpers). Small backend docstring wording tweak.
Switch connection string encoding from the old base64 blob to a readable sk1:// URL format (host/token[?exp&insecure]) across agent, backend, and frontend. Update encode/decode logic and extensive tests to validate port preservation, insecure/http handling, expiry parsing, and input validation; adjust agent pairing UI to accept the new scheme. Add ServerKit Agent GUI plugin: backend blueprint endpoints for capabilities, frame capture and synthetic desktop, plugin metadata; frontend plugin components (ServerGui, Launcher, SyntheticDesktop), styles, and manifest entry to enable streaming screenshots with a synthetic fallback. Also add Windows service stop waiting to avoid start races after pairing, and document SERVERKIT_PUBLIC_URL in env examples.
One-click PowerShell entry point that spins up Ubuntu/Debian VMs in parallel, installs ServerKit from the local working tree (not origin/main), runs a pytest API harness against the live panel, and aggregates results into a self-contained HTML report. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PowerShell 5.1 reads .ps1 files as ANSI by default; em-dashes and check marks were mangled into bytes that broke the parser. Replaced with ASCII equivalents so the script runs under stock Windows PowerShell without needing pwsh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes a cascade of PowerShell + Multipass quirks found during the first real run: - Rewrote orchestrator sequentially. Start-Job's separate runspace was hiding the real errors below behind opaque "method on null" failures. - Removed `function Mp` helper. PowerShell ships a built-in alias `mp` for Move-ItemProperty that silently shadows same-named functions and turned multipass calls into Move-ItemProperty calls. Inline `& $MpExe` everywhere instead. - Lowered ErrorActionPreference from Stop to Continue. Multipass writes harmless "cannot set permissions" warnings to stderr when transferring to NTFS; Stop turned those into fatal aborts. - Stopped piping `multipass exec` stdout through Tee-Object. The pipe could stay open indefinitely after the remote command exited, hanging the orchestrator for 45+ min on a finished install. Now redirects to a file on the VM and only checks the exit code. - Tightened tar excludes. Tarball dropped from 305 MB to 20 MB. - Added `npm ci` in vm-install.sh after the local-code overlay. The original installer ran `npm ci` from origin/main, then the overlay dropped in a dev package.json that needed different deps, causing a spurious frontend build failure. - Added -ReuseVm flag for re-running just the harness against an already-installed VM (skips the 20-min launch cost). First successful end-to-end run on Ubuntu 24.04: 10/10 tests passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
backend: move load_all_plugins(app) inside the with app.app_context() block, after MigrationService.check_and_prepare(app). The loader queries the installed_plugins table; previously it ran before migrations created that table, causing "no such table: installed_plugins" errors in journal on every cold start. Surfaced by the new E2E harness (visible in the journalctl pane of the report). scripts/test: live HTML report. - Orchestrator writes state.json after each phase + regenerates report.html, so opening the page shows real-time progress. - Browser auto-opens the report when the run starts; page auto-refreshes every 8s while a run is in progress. - Added per-VM 'Stage' field (pending -> transferring source -> installing -> running pytest -> done). - Fixed empty "Details" cells rendering as black boxes; now show "-". - Pulsing dot indicator in the live banner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without this import, SQLAlchemy's db.metadata doesn't know about the installed_plugins table, so MigrationService.check_and_prepare's db.create_all() never creates it on fresh installs. The plugin loader then fails on cold start with "no such table: installed_plugins". Pairs with the previous fix that moved the plugin loader to run after the migration step. Both are required; this one is what Copilot's PR review meant by "export the model from app/models/__init__.py". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes for the race where the browser opens report.html before any
VM has registered:
- Orchestrator seeds state.json with {"running":true,"vms":{}} the
instant OutDir is created, so a refresh before the first VM is added
still has a "running" signal.
- report.py treats missing state.json as RUNNING (not FAIL), and a VM
with a non-done stage but no install-status file as RUNNING in its
per-VM badge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
backend/app/services/plugin_service.py: - Zip Slip defense: new _safe_extract_path() validates every archive member against the destination root (rejects absolute paths, ../ segments, and entries that resolve outside backend/frontend dest). All zip extract paths route through it. - Plugin requirements.txt no longer auto-installs. Running pip on an untrusted plugin gives setup.py hooks code-exec at the backend's privilege level. Operators must opt in with SERVERKIT_ALLOW_PLUGIN_PIP=1; otherwise the file is saved to the plugin dir for manual review. - New _attach_status_guard() registers a per-blueprint before_request that returns 503 if the plugin's DB status is not 'active'. Flask can't unregister blueprints from a running app, so the in-DB status is now authoritative at request time — disabling a plugin actually stops serving its routes. - New _update_plugin_metadata() syncs all mutable manifest fields (display_name, description, author, homepage, repository, license, category) on reinstall. Previously these went stale after the first install. backend/app/api/plugins.py: - /install endpoints no longer echo raw exception strings to clients (could leak paths, lib versions, SQL fragments). Log full traceback server-side with a short correlation ref returned to the client. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multipass on Windows only ships Ubuntu images. To cover Debian 12,
Fedora, and Rocky 9 too, add a parallel Vagrant runner that uses the
Hyper-V provider (no VirtualBox conflict with Multipass).
scripts/test/
- vagrant/Vagrantfile: parameterized via SK_BOX, SK_VM_NAME env vars;
Hyper-V provider with linked clones; bootstraps rsync + curl.
- full-stack-test.ps1:
- DistroMap replaces ImageMap; each entry tags backend (multipass/
vagrant), image/box name, login user.
- Default Distros now: ubuntu22, ubuntu24, debian12, fedora, rocky9.
- Backend auto-detection: if Vagrant isn't installed, vagrant distros
silently skip rather than failing. Same for Multipass.
- Test-VagrantInstall mirrors Test-VmInstall using `vagrant upload` /
`vagrant ssh -c`, per-VM VAGRANT_CWD under OutDir for state.
- Teardown dispatches by backend (multipass delete vs vagrant destroy).
- README updated with the new prereqs and distro coverage table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vagrant + Hyper-V requires the process itself to run elevated; being in the Hyper-V Administrators group is necessary but not sufficient. Without this guard, the script would launch, pack the source, then hit a wall on 'vagrant up' with a permission error mid-run. Now it checks early: if any requested distro uses the Vagrant backend and the script isn't running elevated, print exact instructions and exit. Multipass-only runs (Ubuntu) still work without admin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without this, vagrant up halts on first boot asking the operator to choose a Hyper-V switch from a numbered menu, which breaks unattended test runs. Hard-code "Default Switch" (Hyper-V's built-in NAT switch, present on every Hyper-V install), overridable via VAGRANT_HYPERV_SWITCH_NAME env var for users who want a different network. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the orchestrator redirected vagrant up's stdout+stderr to a log file, which meant the box download progress was invisible and any interactive prompts (like "choose a Hyper-V switch") were silently swallowed while vagrant blocked on stdin. The operator had no signal the run was hung. Use Tee-Object so the operator sees download progress + prompts live in the terminal, while the log file still gets a copy for later inspection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit added Tee-Object so vagrant up output was visible in the terminal, but Tee-Object's pipeline output then leaked into Test-VagrantInstall's return stream — the function returned vagrant's entire log instead of "OK", so the final summary reported every passing Vagrant VM as a failure. Append `| Out-Host` after Tee-Object to consume the pipeline. The operator still sees the live output; the function's return stays clean. The previous Debian 12 run was actually all-green (10/10 pytest passed, 0 plugin-loader errors) — only the summary line was wrong. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Merges ~2 months of
devintomain, spanning v1.4.14 → v1.6.2 (71 commits, +66k / -11k). The PR started as "add plugin system" but grew to cover several parallel initiatives. Major themes:PluginLoader, contributions SDK, builtin extensions, Marketplace UI, and a first real plugin (serverkit-gui). Newsk1://connection-string format.system_infopush, WebView2 console, Windows service support, ARM64 + MSI builds, WS flap detection w/ permanent poll fallback, resilient capability/sysinfo delivery on transient/pollfailures, auto-correct staleonlinestatus.JobProgressModal, deployments, pyenv runtime mgmt, Cloudflared tunnel mgmt (incl. panel-driven login)..ui-*classes, dialog / badge / alert revamp, unified dev launcher, developer Style Guide page, EmptyState improvements, Button sizing.008_fix_server_metrics_id_type.Outstanding from Copilot review
Real issues from the earlier plugin-code review, tracked here:
plugin_service.pyextract loop — validate member pathspip installfrom pluginrequirements.txt— gate behind env flag or per-plugin venvenable_plugin/disable_plugindon't actually unregister blueprints — enforce status at request time or require restartinstalled_pluginsand export the model fromapp/models/__init__.pyload_all_plugins(app)runs before migrations — move afterMigrationService.check_and_prepare(app)encodeURIComponent(status)inservices/api/plugins.jsSuspense/userimportsTest plan
installed_pluginstable created before plugin loader runs)serverkit-guiplugin loads viask1://connection stringJobProgressModal.ui-*SCSS migration🤖 Generated with Claude Code