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 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"]
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
[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.
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