Repository navigation
plugin development
Pre-alpha. Two kinds can be written, packaged and checked with the current build: viewer plugins and secret sources (a password manager as the source of a connection's secret). rCommander does not ask a secret source yet - connections take their secrets from one in a later release. The contract (
rcommander:plugin@0.3.0) may still change before its 1.0; a plugin is rebuilt against a new one, and the host still runs plugins built against 0.1.0 and 0.2.0.
An rCommander plugin is a signed WebAssembly component. It runs in a sandbox with no file system, no processes, no network and no WASI: it reaches exactly the interfaces its kind is made of and its manifest grants, and nothing can be asked for at run time. rCommander renders what a plugin returns - a plugin never draws a window of its own. Installing and using plugins is Plugins.
- Rust with the target
wasm32-unknown-unknown(rustup target add wasm32-unknown-unknown). -
wasm-tools1.261.0, the version rCommander's own plugins are made with:cargo install wasm-tools --version 1.261.0 --locked. -
rcommander.exeof the release you build for:rcommander plugin ...writes, packages, signs and checks plugins.
On Windows rcommander.exe is a program with a window: started from a console it prints into
that console, but the prompt may come back before it is done. In cmd start /wait rcommander plugin ..., in PowerShell Start-Process -Wait, waits for it and its exit code.
rcommander plugin new --type viewer --out my_viewer
cd my_viewer
cargo test
cargo build --release --target wasm32-unknown-unknown
wasm-tools component new target/wasm32-unknown-unknown/release/my_viewer.wasm -o target/my_viewer.wasmplugin new writes a small, working viewer: CSV and TSV files as a Markdown table, any other
file as text. It compiles, packages and passes conformance before you change a line, so the first
failure you see is about your code. The folder's name becomes the crate's (--name sets
another), and the scaffold gets:
- its own id, a UUIDv7 in
manifest.toml- keep it across versions; - its own key pair: the private half in
plugin-signing.key, the public half inmanifest.toml. A.gitignorewritten before the key keeps it,target/and*.rcplugout of version control; -
src/lib.rs(the logic, with what the host promises a viewer at the top),src/table.rs,src/types.rs(the contract's shapes as plain Rust),src/lib_tests.rs- all of it runs withcargo testwithout a WebAssembly toolchain; -
src/guest.rs, the translation into the contract's vocabulary, built only forwasm32withwit-bindgen0.62; -
wit/rcommander.wit, the contract;locales/en.json;README.md;CHANGES.md, a## <version>section per version in one to three sentences for the people who install it.
rcommander plugin new --type secrets --out my_sourceA secret source answers rCommander while it connects: status (ready, locked, unreachable,
unlicensed), search (ids, titles, users and addresses of the entries matching a query - never a
value) and get (one secret of one entry: password, private key, passphrase or the current
one-time code). It receives no secret of rCommander's, and it never asks for a manager's master
password: a locked manager answers locked and the person unlocks it in the manager itself.
It reaches its manager through http, granted in the manifest as one port of this machine:
[capabilities]
net_http = { loopback = 8087 } # plain HTTP to localhost:8087, nothing elseThe install preview shows the port, so installing is the person's confirmation, and an update
that moves it asks again. A request to anything else - another port, another host, https,
rCommander's own MCP port - is refused before it leaves; a redirect is answered, never followed;
an answer larger than [limits] max_response_bytes is refused. The time a call waits for its
requests does not count against the call's own time limit - each request at most up to its own
limit, and 60 seconds per call in all; a call whose requests take longer together ends with
plugin_http_time_budget. The scaffold speaks to an example bridge with three plain-text
requests; replace them with your manager's local API.
-
The file is a handle, never a path.
sourcegives its name, size and bytes, read in windows of at most 1 MiB. There is no file system to reach anything else. -
One document, several views.
opennames every output the document offers -text,highlighted(coloured text),markdown,image,tree,pages- the first shown, each one listed in the manifest's[viewer] outputs; the host refuses any other. The person switches between them with 7. -
One store per document. What
openlearned stays for the calls after it until the file is closed; memory, fuel and time are counted per call. Reading a byte earns fuel, so a viewer that streams its file is not starved by its size. -
Moving on cancels. Quick View moves to the next file: ask
should-stopbetween windows and end withcancelled. A call that keeps running is ended. -
Classes, not colours. Coloured text names a token class per span (
key,string,number,comment, ...); the theme colours it, and only the lines on screen are asked for. - Every answer is checked. Outputs the document did not name, tokens outside their line, a tree too deep or too wide, text over the limit, a raster of the wrong size - refused or cut, and a plugin that crashes three times in five minutes is suspended.
manifest_version = 1
plugin_type = "viewer"
api_version = "0.1.0" # the contract built against
id = "019d0000-0000-7000-8000-0000000000a1" # UUIDv7, stable across versions
name = "CSV table"
version = "1.0.0" # strict SemVer
key_id = "csv_table-release-v1"
public_key = "...base64 Ed25519..."
[match] # which files rCommander offers the plugin
extensions = ["csv", "tsv"]
names = [] # whole file names: ["Dockerfile", "*.mk"]
magic = [] # [{ offset = 0, hex = "25504446" }]
max_size = 268435456
[viewer]
outputs = ["markdown", "text"]
[capabilities] # a viewer is granted nothing
[metadata]
description = "Shows CSV files as a table."
author = "Jane Doe"
license = "MIT"
min_app_version = "0.1.0" # the first rCommander that accepts api_version
# ("0.19.0" or later with [match] names)
[limits] # optional: memory_bytes, fuel, timeout_milliseconds
timeout_milliseconds = 30000At most 1 MiB, unknown keys refused. [[settings]] declares the plugin's own settings
(type = "choice" with values, or "text"), shown on its page in Settings; a text setting
with masked = true (a licence key, a token) is typed hidden and never shown to an AI assistant. A failure code
the plugin reports starts with its id (<id>.<condition>) and has its text in
locales/en.json; the host drops any other code.
[match] claims files three ways: extensions (what follows the last dot, without it, any
case), names (whole file names) and magic (bytes at an offset). A file larger than
max_size is never offered.
names takes whole file names, never a folder: compared without regard to case, with * as the
only wildcard (Dockerfile, Dockerfile.*, *.mk, .env.*). At most 32 names of at most 64
characters each; no path separator or character Windows forbids in a name, none twice, and none
made of * and . alone. A manifest with names needs min_app_version = "0.19.0" or later -
an older rCommander refuses the package.
rCommander asks the plugins in this order, each group by plugin name, up to four in all, and the
first that does not turn the file down in probe shows it:
- the plugin the person gave the extension to (Extensions given to a plugin);
- an exact name in
names(Dockerfile,nginx.conf); - the extension in
extensions; - a name pattern with
*innames(Dockerfile.*,*.conf); -
magic.
So an exact name wins over every extension, and an extension over a pattern: Dockerfile.dev
goes to a plugin claiming Dockerfile.*, while Dockerfile.toml stays with the one claiming
toml. A plugin that claims broadly - *.conf, say - should decline in probe the files it
cannot read, so the next one gets them.
rcommander plugin package --manifest manifest.toml --component target/my_viewer.wasm \
--locales locales --key plugin-signing.key --output my_viewer.rcplug
rcommander plugin verify my_viewer.rcplug --trusted-key my_viewer-release-v1=<public_key>
rcommander plugin conformance my_viewer.rcplug --trusted-key my_viewer-release-v1=<public_key>-
packagechecks the manifest, the translations and the component's imports, signs the package and verifies the result.--keymay be left out whenRCOMMANDER_PLUGIN_SIGNING_KEYholds the key's PEM text.--developmentwrites an unsigned package, which only development mode installs - for your own tests, never for a release. -
verifychecks a package as installing it would and says what it is; for a key it does not know it prints the fingerprint to compare. -
conformanceanswers whether this rCommander would run your package, and exits 1 if not.--jsonprints the report for a CI job. -
keygen --out <file>writes another key pair, for a release key kept apart from the oneplugin newwrote. A key file is never overwritten.
A .rcplug is a ZIP of exactly manifest.toml, component.wasm, signature.ed25519 and
locales/<lang>.json; the Ed25519 signature covers all of them, so neither the component nor a
translation can be swapped after the release.
| Check | What it asks |
|---|---|
package |
The package verifies as installing would: archive, manifest, API version, translations, signature, limits, and every import the manifest grants |
world |
The component exports the world its kind requires |
viewer_empty_source |
probe, open and the first output the document names, on an empty file named like one [match] claims, end in an answer or a refusal - never a crash, an exhausted budget or an answer against the rules |
secrets_no_service |
A secret source's status and a search with its granted port refused, as when the manager is not running, end in an answer or a refusal - never a crash |
It does not check that your viewer shows a real file correctly: the report says what was verified, not that the plugin is good.
A component may import only rcommander:plugin interfaces - and of those only what its kind
and manifest grant. One that imports anything else (a wasi: interface a dependency pulled in)
is refused when packaged, installed and run. wasm-tools component wit my_viewer.wasm shows its
imports; rCommander's scripts/check-plugin-imports.sh and the CI workflow refuse any outside
the contract.
sdk/ci/plugin.yml of rCommander's repository is a GitHub Actions workflow for a plugin's own
repository: test, build, refuse imports outside the contract, package, verify and run
conformance on a Windows runner with the rCommander release it pins (RCOMMANDER_VERSION).
Only a push to the default branch or a tag signs, with the key from the repository secret
PLUGIN_SIGNING_KEY; a pull request is checked unsigned in development mode.
The scaffold and the CI workflow are MIT licensed, unlike rCommander itself (GPL-3.0-or-later): a plugin built from them may carry any licence.
wit/rcommander.wit is the contract. The reference below is generated from it.
Every world a plugin can be built against, and every interface in the contract, as the WIT
states them. A world's name is also the scaffold's: rcommander plugin new --type <name>
for the world <name>-plugin.
| World | Imports | Exports | Scaffold |
|---|---|---|---|
viewer-plugin |
host, source, sink, settings
|
viewer |
--type viewer |
secrets-plugin |
host, settings, http
|
secrets |
--type secrets |
enum failure-kind
not-found, exists, access-denied, not-supported, bad-data, wrong-password, cancelled, io, other
record failure
| Field | Type |
|---|---|
kind |
failure-kind |
code |
option<string> |
params |
list<tuple<string, string>> |
enum entry-kind
file, folder, link
record entry
| Field | Type |
|---|---|
name |
string |
kind |
entry-kind |
size |
option<u64> |
modified |
option<string> |
attributes |
u32 |
record source-info
| Field | Type |
|---|---|
name |
string |
size |
u64 |
modified |
option<string> |
type source = u32: a handle valid for this call or session, never a path
type sink = u32
always; rDownloader's host without wait and secret-available
log: func(level: string, message: string)now-unix-seconds: func() -> u64random-bytes: func(count: u32) -> list<u8>
by kind; rDownloader's source: a handle, never a path
Uses failure, source, source-info from types.
info: func(src: source) -> source-info-
read-at: func(src: source, offset: u64, length: u32) -> result<list<u8>, failure>- <= 1 MiB progress: func(done: u64, total: option<u64>)should-stop: func() -> bool
by kind; rDownloader's sink, addressed by handle
Uses failure, sink from types.
-
write-at: func(out: sink, offset: u64, bytes: list<u8>) -> result<_, failure>- <= 1 MiB committed: func(out: sink) -> u64sync: func(out: sink) -> result<_, failure>progress: func(done: u64, total: option<u64>)should-stop: func() -> bool
by manifest; rDownloader's destination-settings
-
setting: func(name: string) -> option<string>- The value of the declared settingnamefor the call in progress: what the person chose, else the manifest's default.nonefor a name the manifest does not declare.
capability net_http; rDownloader's http without templates or cookies
Uses failure from types.
-
request: func(method: string, url: string, headers: list<tuple<string, string>>, body: list<u8>) -> result<http-response, failure>- One request to a target the manifest grants; a redirect is answered, never followed.
record http-response
| Field | Type |
|---|---|
status |
u16 |
headers |
list<tuple<string, string>> |
body |
list<u8> |
Uses failure, source, sink from types.
probe: func(src: source) -> boolopen: func(src: source, purpose: purpose, theme: theme) -> result<document, failure>-
text: func(out: sink) -> result<string, failure>- the encoding written -
tokens: func(first-line: u32, count: u32) -> result<list<list<token>>, failure>- highlighted -
image: func(out: sink) -> result<string, failure>- png, jpeg, gif, bmp, webp, rgba:<w>x<h> page: func(index: u32, width: u32, height: u32, out: sink) -> result<_, failure>children: func(node: option<u64>) -> result<list<tree-node>, failure>search: func(needle: string, from-page: u32, backwards: bool) -> result<option<u32>, failure>thumbnail: func(src: source, size: u32, out: sink) -> result<string, failure>
enum purpose
lister, quick-view
enum output
text, highlighted, markdown, image, tree, pages
record theme
| Field | Type |
|---|---|
dark |
bool |
background |
string |
foreground |
string |
accent |
string |
record document
first shown
| Field | Type |
|---|---|
outputs |
list<output> |
title |
option<string> |
pages |
option<u32> |
record tree-node
| Field | Type |
|---|---|
id |
u64 |
label |
string |
value |
option<string> |
has-children |
bool |
enum token-class
keyword, key, string, number, literal, comment, punctuation, operator, type-name, function, invalid
record token
UTF-8 bytes within its line
| Field | Type |
|---|---|
start |
u32 |
len |
u32 |
class |
token-class |
Uses failure from types.
status: func() -> source-state-
search: func(query: string, limit: u32) -> result<list<secret-entry>, failure>- never a value -
get: func(id: string, field: secret-field) -> result<string, failure>- the one secret answer
enum source-state
ready, locked, unreachable, unlicensed
enum secret-field
password, private-key, passphrase, totp
record secret-entry
| Field | Type |
|---|---|
id |
string |
title |
string |
user |
option<string> |
url |
option<string> |
Getting started
Using rCommander
Developing
Reference