#Plugins
A GeoSpaX plugin is a folder with a manifest and one or more tools. Tools land in the
Toolbox (right pane, searchable, both modes), on the ribbon and in the Plugins pane (bottom
dock, Pro); commands land on the ribbon and in the command palette (Ctrl+K). A plugin with more than
40 tools gets one ribbon button that opens the Toolbox on it.
Bundled: plugins/geolibre-tools brings 1,062 GeoLibre tools (raster 544, vector 342, LiDAR 68,
conversion 53, hydrology 32, terrain 19, other 4), see §1a.
The point of the design: GeoSpaX reads the manifest formats of the four ecosystems people already write for, so the same tool works whether it was authored for GeoSpaX, RustGIS, GeoLibre, QGIS or ArcGIS Pro. Where a tool cannot run here, GeoSpaX says so instead of pretending.
#1. What is compatible with what
| Ecosystem | What it is | Manifest GeoSpaX reads | Runs? |
|---|---|---|---|
| GeoSpaX | folder with plugin.yaml |
plugin.yaml / plugin.yml |
✅ fully — process, python, r, wasm, recipe |
| RustGIS (opengeos/RustGIS, Apache-2.0) | .rgplugin = zip of plugin.json + plugin.wasm |
plugin.json (commands, tools, params, permissions) |
✅ the WASM module, through a WASI runner (wasmtime/wasm3/deno) |
| GeoLibre (opengeos/GeoLibre, MIT) | TS/React plugin (plugin.json) + WASM tool bundles (geolibre-rust, WASI over /work) |
plugin.json, tools.mjs; the whole tool runner through the bundled geolibre-tools plugin |
✅ its 1,062 tools, bundled (§1a); ❌ its UI (React components) |
| QGIS | Python plugin; metadata.txt + __init__.py |
metadata.txt (hasProcessingProvider=yes) |
✅ its processing algorithms, through qgis_process list/help/run --json; ❌ its PyQt UI |
| ArcGIS Pro | .esriAddInX = zip of Config.daml + .NET assembly (+ Toolboxes/*.pyt) |
Config.daml, *.pyt |
✅ Python toolbox (.pyt) tools through python3 + arcpy; ❌ the .NET assembly (no CLR) |
Honest limits
- A QGIS UI plugin drives the QGIS GUI through PyQt — there is no way to host it. Its
processing algorithms are a clean CLI contract (
qgis_process), and those we run. - An ArcGIS Pro add-in's buttons are .NET (
ProAppModule.dll). We import the ribbon declaration (tabs, groups, buttons fromConfig.daml) and can execute Python toolbox tools; the compiled tool bodies have to be ported. GeoSpaX labels themneeds arcpy/ "the .NET assembly cannot run" rather than silently dropping them. - RustGIS plugins are WASM and sandboxed; GeoSpaX runs the same module with the same JSON contract,
so a
.rgpluginis portable between the two apps. Itspermissionsare respected:editis required before a plugin may change the project,filesbefore it may name a file.
#1a. GeoLibre tools (bundled)
plugins/geolibre-tools wraps GeoLibre's tool runner, geolibre-cli.wasm from the npm package
geolibre-wasm (MIT; opengeos/geolibre-rust, built on
John Lindsay's whitebox_next_gen), as an ordinary GeoSpaX process plugin:
plugin.yamlandtools.jsonare generated bygen.mjsfrom the runner's own manifests (geolibre-cli manifests): names, summaries, categories, and a form per tool. Vector inputs become layer pickers; raster, LiDAR and table inputs take a path; enums become choices; outputs are not asked for.run.mjsruns the tool under Node's WASI (Node.js 18+): the directories of the input files are mounted read-only-by-convention as/in0,/in1…, outputs go to/out=<state>/plugin-results/ geolibre-tools/, and the files written are returned in"outputs", so they join the map.- The runner is not redistributed. On first use
runtime.mjsdownloads the pinned package (1.7.1) from the npm registry, checks its SHA-512 against the registry's published integrity, and cachesgeolibre-cli.wasmin~/.cache/geospax/. Offline:node run.mjs --fetchwhere there is internet,geolibre-cli.wasmnext to the plugin, orGEOSPAX_GEOLIBRE_WASM=/path/to/it. - Upgrading: change
VERSIONandINTEGRITYinruntime.mjs, thennode gen.mjs.
Measured here: Buffer Vector on 1,500 points → 1,500 polygons added to the map in 0.35 s through the API; the first-use download verified and byte-identical to the npm package.
#1b. Conservation, raster and species tools (bundled)
plugins/gsx brings the conservation, biodiversity, raster and species-distribution tools into GeoSpaX 3 as a Node tool library (library: true: its tools run from recipes, the
API and MCP but are not listed). The app shows them through the 22 default domain plugins
plugins/geospax-<domain> (default: true), whose manifests plugins/gsx/gen-manifest.mjs writes and
whose run: is the library's run.mjs. See GSX.md.
Two manifest keys serve this: library: true (a tool set other plugins run; hidden from the ribbon,
Toolbox and plugin list) and default: true (honoured only for plugins shipped with GeoSpaX: listed as a
default plugin, not uninstallable).
#2. Plugin folders
bundled plugins $GEOSPAX_PLUGINS, else ./plugins (source checkout), plugins\ next to the
.exe (Windows) or /usr/share/geospax/plugins (.deb, Docker)
<state>/.geospax/plugins/ always read (state = the first --data folder); imports go here
--plugins DIR extra folder (repeatable)
~/GeoSpaX/plugins the installed desktop app reads this too
geospax-server plugins list prints what it found:
example-python native python 2 tool(s) runnable
field-recipes native recipe 2 tool(s) runnable
hello-rustgis rustgis wasm 1 tool(s) needs plugin.wasm next to plugin.json
save_attributes qgis process 0 tool(s) needs qgis_process
Field Tools arcgis python 1 tool(s) needs arcpy (for .pyt tools)
#3. Writing a GeoSpaX plugin
plugin.yaml:
plugin: example-python # id: lowercase, no spaces
version: 0.1.0
title: Example Python tools
description: Two tools that show the contract.
author: You
license: MIT
homepage: https://example.com
kind: python # process | python | r | wasm | recipe
run: python3 ${plugin}/tools.py ${tool}
tools:
- id: layer-report
name: Layer report
summary: Row count, field list and a value count for one layer.
group: Example # ribbon group / toolbox name
icon: chart # any icon name in web/src/commands.ts ICONS
params:
- { name: layer, label: Layer, kind: layer, required: true }
- { name: field, label: Count values of, kind: field }
- id: point-grid
name: Point grid
summary: Hexagon or square grid over a layer's extent.
group: Example
icon: grid
params:
- { name: layer, label: Over, kind: layer, required: true }
- { name: size, label: Cell size (m), kind: number, default: 500, required: true }
- { name: shape, label: Shape, kind: choice, default: hex, choices: [hex, square] }
commands: # optional ribbon buttons
- { id: about, title: About this plugin, icon: check, group: Example }
Parameter kind: number · text · bool · layer · field · file · choice.
${plugin} expands to the plugin folder, ${tool} to the tool id, ${workdir} to a scratch dir.
#Run-time contract
GeoSpaX runs run: with this JSON on stdin:
{ "plugin": "example-python", "tool": "layer-report",
"params": { "layer": "field_samples.geojson", "field": "land_use" },
"layers": ["field_samples.geojson", "incidents.geojson"],
"layer_sources": { "field_samples.geojson": "/home/me/GeoSpaX/Data/field_samples.geojson", "…": "…" },
"workdir": "/tmp/geospax-plugin-work",
"out_dir": "/home/me/GeoSpaX/Data/.geospax/plugin-results/example-python" }
and expects JSON on stdout:
{ "ok": true, "message": "report for field_samples.geojson", "outputs": ["…/out.geojson"] }
A tool can name its ribbon tab with tab: (home, data, style, analyze, imagery,
modelling, automate, view, help); tools that name none are listed in the Explorer's Plugins and the Tools tab, grouped by
their group.
A plugin that reads GeoJSON sets geojson: true in plugin.yaml: every layer named by a layer
parameter is then written to the workdir as GeoJSON, whatever its source format, and passed in
layer_geojson ({"layer id": "/tmp/…/gsx-layer.geojson"}).
Rasters in and out. A parameter of kind raster (one) or rasters (several) is a raster on
the map. GeoSpaX hands each one over as a plain float32 grid in raster_grids
({"raster id": "/tmp/…/gsx-climate.grid.json"}): the JSON header has width, height, bands,
band_names, the GDAL transform, the crs (WKT), title, kind, units, and data, the path of
the bands as little-endian float32, row-major, north up, band after band, NaN = no data. All rasters of
one run are resampled onto the first one's grid (at most 1,500 cells a side), so a tool can stack them
cell by cell with no GDAL of its own. To return a raster, write the same pair (<name>.grid.json + its
float32 band) into out_dir and list the .grid.json in outputs; GeoSpaX turns it into a GeoTIFF and
adds it to the map, using the header's title, kind (suitability, change, classes, slope,
ndvi … pick the styling) and classes ([{value, label, color}]).
The answer may also carry table ([["label", value], …]) and notes (strings): the tool's
dialog shows them under the form, so a tool can report numbers without making a layer.
Files listed in outputs are added to the map: GeoTIFFs as rasters, anything GDAL reads as vector
layers. The reply to POST …/run names them in "added", and the app styles them (AutoMap) and zooms to
them. Write results into out_dir. A kind: process plugin whose program (the first word of run:)
is not on PATH is listed as needs <program>.
A non-zero exit turns the first line of stderr into the error shown in Explorer › Plugins › Manage plugins…. That is the
whole ABI, so the same plugin shape works for a Rust binary, an R script
(run: Rscript ${plugin}/tools.R ${tool}) or a WASI module
(run: wasmtime run --dir ${workdir} ${plugin}/tools.wasm -- ${tool}).
kind: recipe plugins ship ordinary GeoSpaX recipes in recipes/ and their tools run through the
automation engine — so they get triggers, run history, artifacts and Auto switches for free
(see plugins/field-recipes).
#4. Installing plugins
- UI: Explorer › Plugins › Manage plugins… › paste a folder or package path › Install.
- CLI:
geospax-server plugins import ./my-plugin [--dir PLUGINDIR],plugins list,plugins check ./my-plugin. - Removing one: Uninstall on the plugin's card (it asks twice),
DELETE /api/plugins/{id}, orgeospax-server plugins uninstall ID [--dir PLUGINDIR]. Only plugins that were installed into the plugin directory can be removed; one that lives in a folder you pointed at with--pluginsis refused, with a message saying where it actually lives — GeoSpaX will not delete your working copy just because an id matched. Installing is what makes a plugin removable, so dev folders stay safe. - Packages (
.rgplugin,.esriAddInX,.zip) are unpacked into the plugin folder. Members are size-limited (64 MB),../and absolute paths are rejected, and the executable bit is preserved. - Package files that are not installed are still listed (read-only, from inside the zip) so you can see what is in them before installing.
#5. HTTP API
| Route | What |
|---|---|
GET /api/plugins |
{plugins, runtimes, dirs} — installed plugins and which runtimes this machine has |
POST /api/plugins/reload |
re-scan the plugin folders |
POST /api/plugins/import |
{"path": "…"} → install a folder or package |
GET /api/plugins/{id} |
one plugin |
POST /api/plugins/{id}/tools/{tool}/run |
{"params": {…}} → run a tool; {"result", "added", "stderr"} |
#6. Credits
The plugin model follows RustGIS (docs/plugins.md, Apache-2.0): a manifest plus a sandboxed
WASM module, hash-pinned packages, permissions, and commands that the ribbon, the palette and the
CLI all call. The WASI "tools over /work" contract follows GeoLibre's geolibre-rust tool
runner (MIT). QGIS and ArcGIS Pro compatibility uses their public, documented CLI surfaces
(qgis_process, Python toolboxes). See THIRD_PARTY_NOTICES.md.
Plugin manager
Open Plugins › Manage plugins. The tabs are All, Installed, Not installed, Upgradeable, Install from ZIP and Settings. Untick a plugin to switch it off: it stays installed but its tools are hidden. Built-in plugins cannot be switched off or removed.
Under Settings, add a repository (https, file:// or a local path) and press Reload repositories. A repository is a JSON index:
{"plugins": [{"id": "my-plugin", "name": "My plugin", "version": "1.2.0",
"download": "https://example.org/my-plugin-1.2.0.zip", "sha256": "optional hex digest"}]}
Where repositories overlap, the newest version wins. A package whose SHA-256 does not match is refused. QGIS plugins.xml repositories are not read.