Memona plugins
Plugins connect Memona to additional filesystems through a shared API. The app and its folder agents use the same connection service and permission checks.
- Using Memona: start with Using plugins.
- Building a plugin: follow Build a plugin.
- Looking up a function: open the generated API reference.
- Publishing: visit the plugin catalog and verify your email before uploading a release.
How it works
A plugin is a portable WebAssembly component. Memona’s Rust backend hosts it using Wasmtime and calls the filesystem functions described by WIT. WIT defines the contract; WASI provides selected system interfaces. The backend supplies network access according to the connection’s grants.
Plugin authors can use the Rust SDK without copying Memona’s WIT files into their own repository. There is no Python or JavaScript backend interpreter. The same installed backend serves both the native app window and browser clients connected to it; plugins do not depend on a browser tab remaining open.
Initial scope
The initial API covers filesystem providers, connection configuration and agent file tools. WebDAV and IPFS integrations are planned provider implementations; this release does not bundle them. The repository includes a test component that demonstrates the contract.
Custom viewers and editors using Web Components are a future extension point. Version 1 packages do not execute frontend JavaScript or install scripts. Remote offline synchronization is also outside this initial API delivery.
Memona keeps its existing folders-as-pages model. Plugins do not introduce a mandatory vault structure.
Using plugins
Open Plugins from Memona’s menu. The dialog has three sections:
- Browse searches the public catalog and shows package details.
- Installed manages installed versions and enabled state.
- Connections configures each provider and its agent permissions.
Install and update
Select a catalog entry to inspect the publisher, version, description and requested network access. Choose Install to download that exact release. Memona validates the downloaded package and its digest before activation. You do not need a publisher account to browse or install.
Updates are explicit. A failed update leaves the previous installed version in place. Review the requested access when a new version requires additional permissions; updating does not silently grant them.
Disabling a plugin stops its connections and prevents new operations. Removing a plugin does not delete files stored on a remote service. If the catalog is unavailable, installed plugins can still be managed; remote operations still need access to their configured remote service.
Connections and agents
A provider can have several connections with separate configuration. Set agent access independently for each connection:
| Setting | What Memona’s agent file tool can do |
|---|---|
| Off | The connection is not available to agents. |
| Read only | Inspect folders and read files. |
| Read and write | Read and request supported changes. |
The provider may itself be read-only. Granting write access cannot add an operation that the provider does not support. Agent grants apply to Memona’s remote-file tool; they do not change what an external agent’s independent shell or local filesystem tools can access.
Enter the exact destinations in Allowed hosts before reconnecting, for
example files.example.com. A new connection starts with no allowed hosts.
For a LAN or loopback service, also enable Private network explicitly.
Credentials alone do not grant network access.
Credentials belong to a connection. Secret fields are kept in backend memory and are not shown back to the frontend or written to the connection file. After restarting the backend, enter required secrets again before reconnecting.
Publishing
Use the publisher link to open plugin.memona.io. Publishing and email verification happen there. Installation happens inside Memona; there is no custom install-link protocol.
Agent access
Installed filesystem providers share one Memona agent tool, remote_files.
Enable access for each connection in Plugins → Connections. New connections
default to Off. Read only permits browsing and reading; Read and write also
permits supported changes.
An agent first discovers connections:
{"operation": "connections"}
Only connections with an enabled plugin and an agent grant appear. Use the returned connection ID for subsequent calls:
{
"operation": "list",
"connectionId": "the-returned-connection-id",
"path": "/",
"cursor": null,
"limit": 100
}
To read part of a file:
{
"operation": "read",
"connectionId": "the-returned-connection-id",
"path": "/notes.md",
"offset": 0,
"length": 65536
}
File bytes are JSON arrays of integers from 0 through 255. A read includes an opaque revision when the provider supports it and an end-of-file flag. Continue with the next offset to read a larger file. Each call transfers at most 1 MiB.
Changing a file
A write replaces the entire file. Always choose a condition:
| Condition | Behavior |
|---|---|
{"kind":"createOnly"} | Fail if a file already exists. |
{"kind":"ifMatch","revision":"opaque-token"} | Replace only the revision you read. |
{"kind":"overwrite"} | Explicitly replace the current contents. |
Conditional replacement requires provider support. An unsupported condition fails; the host never silently changes it to overwrite. A file larger than the write limit cannot be saved with this API version. Splitting it into multiple writes would repeatedly replace the file, not append chunks.
If a mutation reports outcomeUnknown, inspect the remote state before deciding
whether to retry: the service may have completed it before the response was lost.
These resources are connection-relative paths, not operating-system mounts. An agent’s ordinary shell and local file tools do not automatically access them. Changing the grant or disabling the plugin affects subsequent calls and cancels active work for that connection.
Build a plugin
The initial SDK targets Rust and WASI 0.2 components. Install the component target:
rustup target add wasm32-wasip2
The reference implementation uses Rust 1.98. Plugin source can use compatible Rust releases, but its generated component must satisfy the supported WIT API. Native subprocess, OS-specific and unrestricted networking crates are not automatically available inside a component.
Start with the SDK
The SDK lives at plugin_system/sdk in the
Memona repository. Until a crates.io
release is published, use a checked-out, pinned Memona revision and a path
dependency:
[package]
name = "my-memona-plugin"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
memona-plugin-sdk = { path = "../memona/plugin_system/sdk" }
The path is relative to your own Cargo.toml; adjust it to your checkout. Pin
the checkout revision so your build does not silently pick up an API change.
The SDK includes the canonical WIT and generated Rust types. Implement its
Guest trait and invoke its export! macro for your implementation type. You
do not need wasm-bindgen, wasm-pack, or WIT copies in your plugin repository.
cargo doc also exposes the generated Rust API.
struct MyProvider;
// Implement every method of memona_plugin_sdk::Guest for MyProvider.
// Unsupported optional operations return ErrorCode::Unsupported.
memona_plugin_sdk::export!(MyProvider);
This short excerpt shows registration, not a complete implementation. The full
buildable example is plugin_system/test-component/src/lib.rs. It is a test
fixture, not a WebDAV or IPFS client. From a Memona checkout, build it with:
cargo build --manifest-path plugin_system/Cargo.toml \
-p memona-plugin-test-component --target wasm32-wasip2 --release
Build your own plugin with:
cargo build --release --target wasm32-wasip2
Call the host
Use the SDK’s generated host interface for network requests. For example, this helper retrieves a bounded HTTP response through the connection’s permissions:
use memona_plugin_sdk::memona::filesystem::host;
pub fn get_bytes(url: &str) -> Result<Vec<u8>, host::HostError> {
let response = host::http(&host::Request {
method: "GET".into(),
url: url.into(),
headers: Vec::new(),
body: Vec::new(),
})?;
if !(200..300).contains(&response.status) {
return Err(host::HostError::Unavailable);
}
Ok(response.body)
}
Map HTTP statuses to appropriate filesystem errors in a real provider. Supply authentication through request headers using connection configuration; do not hard-code tokens. The host checks exact destinations and private-network permission and limits request/response bodies to 4 MiB. Reads exported by the filesystem provider have the smaller 1 MiB transfer limit.
Prepare a release
Copy the resulting .wasm component to plugin.wasm, write
manifest.json, and ZIP the files at the archive root. Name the
archive with a .memona-plugin suffix. A directory wrapping the files inside
the ZIP is not accepted.
Use the filesystem behavior guide to handle revisions, unsupported operations and mutation failures correctly. Before publishing, exercise your provider against its real service and test lost connections.
Package format
A .memona-plugin package is a ZIP archive with these root entries:
manifest.json required
plugin.wasm required WebAssembly component
README.md optional UTF-8 text
LICENSE optional UTF-8 text
icon.png optional PNG
Version 1 rejects other entries, duplicate paths, directories, symlinks, encrypted archives and paths that could escape the archive root. There are no install hooks or executable frontend assets.
Manifest
{
"schemaVersion": 1,
"id": "your-publisher/my-provider",
"name": "My filesystem",
"version": "0.1.0",
"apiVersion": "0.1.0",
"description": "Connect to my filesystem service.",
"license": "MIT",
"entry": "plugin.wasm",
"networkHosts": ["files.example.com"],
"providers": [
{
"id": "files",
"name": "My filesystem",
"schemes": ["myfiles"],
"configFields": [
{"key": "endpoint", "label": "Service URL", "kind": "text", "required": true},
{"key": "token", "label": "Access token", "kind": "password", "required": true}
]
}
]
}
Replace the example publisher and host with ones you own. Your verified publisher account must own the publisher portion of the package ID.
| Field | Meaning |
|---|---|
schemaVersion | Manifest shape; currently 1. |
id | Immutable publisher/plugin identifier. |
version | Canonical semantic release version. |
apiVersion | Memona plugin API version; currently 0.1.0. |
entry | Must be plugin.wasm in this format. |
networkHosts | Declared network destinations; connection grants still apply. |
providers | Stable provider identities and declarative configuration fields. |
Publisher/plugin/provider slugs use lowercase ASCII letters, digits and hyphens,
begin and end with a letter or digit, and have at most 64 characters. Human names
can use other languages. Configuration kind is text, password or boolean.
Never place a credential in a manifest or a password default.
An empty networkHosts list declares no network access. A provider for arbitrary
self-hosted endpoints may declare "*"; users still grant exact destinations
per connection. Connection grants never accept a wildcard, and private-network
access requires its separate connection setting.
Limits and compatibility
| Item | Limit |
|---|---|
| Compressed package | 32 MiB |
| Expanded archive total | 128 MiB |
| WASM component | 64 MiB |
| Manifest | 64 KiB |
| README or license | 256 KiB each |
| PNG icon | 1 MiB |
The same validation runs on the store and in Memona. Components must implement
the full provider interface, including optional-operation
methods that may return unsupported, and import only supported host services.
Each published ID/version permanently identifies one byte sequence and SHA-256 digest. Correct a release by incrementing its version. Withdrawing a release prevents new catalog installations; it does not silently remove installed code.
Filesystem behavior
The generated reference lists exact names and types. This page explains the behavior expected across different remote services.
Connection and paths
Memona creates one component instance per connection. open receives the
provider ID and that connection’s configuration and returns its capabilities.
The host owns connection IDs; the plugin receives connection-relative paths.
/ is the connection root. Paths use / separators, cannot traverse above the
root and must never be interpreted as local OS paths. File names are returned
as names, not HTML. A listing is paginated with an opaque cursor and a bounded
limit. Do not reuse a cursor across unrelated listing requests.
Read and revisions
stat identifies a file or directory and may return an opaque revision token.
read takes an offset and maximum length and returns bytes, an optional revision
and an end-of-file flag. Use bounded range reads rather than loading an entire
remote file into guest memory. Do not silently truncate data while claiming EOF.
Revision values are provider-defined. They could be a service ETag or an immutable content identifier. Consumers compare them for equality; they must not parse them as timestamps or assume they are ordered.
Write and conflict handling
The initial write operation replaces a complete file within the host’s transfer limit. It is not an append or streaming-write operation. Large-file publishing must not be simulated by repeated calls that accidentally overwrite each other.
The caller chooses one of three conditions:
create-only: fail if the target already exists.if-match(revision): replace only that revision atomically.overwrite: replacement explicitly requested without a revision condition.
Only advertise conditional-write support if the remote service can enforce it.
A read followed by an unconditional write is not an atomic conditional write.
Return unsupported when you cannot honor the requested condition.
If a connection fails after a write or other mutation may have reached the
server, report outcome-unknown. Do not retry automatically: the operation might
already have succeeded. Consumers can inspect the remote state before deciding
what to do next.
Read-only stores return unsupported for mutations. They still implement all exported methods so the component has a predictable shape. Write permission from the user never overrides a provider’s missing capability.
Changes and cleanup
Providers that support change polling advertise that capability. A poll returns a bounded batch and optional cursor; it must finish promptly. The initial API exposes explicit poll calls. A caller retains its cursor and decides when to poll again; there is no automatic watcher subscription in this version.
close releases provider-owned connection state. Timeouts, disable, disconnect,
update or uninstall may terminate the instance even if cleanup cannot finish.
Never rely on an eventual close callback to commit a pending write.
Errors
Return the closest typed error category. Host/UI messages are generic and localized. Do not put credentials into log messages. An unsupported operation, permission denial, missing file and version conflict are different outcomes; turning all of them into an internal error prevents useful recovery.
Permissions and lifecycle
Network and system services
The host provides bounded HTTP requests through the WIT host interface. Network access requires both a package declaration and a connection grant. Redirects are checked against the same destination policy. A provider must not assume that compiling a native networking crate grants socket or process access.
WASI support is deliberately limited. The plugin does not inherit Memona’s environment variables, local directories, credentials for other connections or subprocess access. Clock/randomness and standard-library plumbing are supplied only as permitted by the host. Filesystem imports do not imply that local host directories are mounted into the component.
Connections to local or private-network services require explicit configuration. Keep declared access as narrow as the service allows. Do not include tokens in URLs or diagnostics; use the connection’s secret fields.
Agent grants
Agent access is independent of network permission. Each connection is off,
read-only, or read/write for Memona’s remote_files tool. The Rust host checks
the current setting before discovery and every operation. Plugins cannot raise
their own grants, and an agent’s permissive CLI mode cannot bypass them.
These controls do not sandbox the external agent’s independently available shell. An agent must use Memona’s tool to access a provider; virtual paths are not automatically mounted as system drives.
Resource limits and cancellation
Each guest instance has a 64 MiB linear-memory limit and a bounded execution budget. A plugin that loops forever or traps is stopped. Transfer sizes, queued work and operation duration are also bounded. Host I/O is asynchronous, but the WASI 0.2 guest interface can look synchronous to its Rust implementation.
Package validation and compilation use separate, short-lived workers with a 30-second deadline per job. The app shares a two-worker limit across both stages; the store permits two validation workers. Cancelling either stage kills and reaps its worker before the slot becomes available again. Only output from Memona’s own compiler worker can be loaded as compiled code; downloaded packages contain portable WASM components.
Worker memory is separate from the guest limit. Linux workers have a 2 GiB virtual-address-space ceiling; macOS workers receive 2 GiB beyond their startup mappings. Windows workers have a 2 GiB committed-memory ceiling. These are process allocation limits, not an identical resident-memory measurement on every OS.
Disabling or removing a plugin prevents new calls and cancels active work. Connection instances do not share mutable guest state or credentials. The backend may remain alive after windows close; all work ends when the backend itself exits.
Future frontend modules
A future Web Component runs in a browser context and has a different permission boundary. Serving JavaScript from a WASM component does not place that JavaScript inside the WASM sandbox. Version 1 does not execute frontend plugin modules.
Publish a release
- Open plugin.memona.io and enter your email address.
- Enter the 16-character sign-in code from your email to create a publisher session.
- Claim a publisher name and prepare a package with that publisher in its ID.
- Upload your
.memona-pluginpackage in the publisher dashboard. - After automatic validation succeeds, the release appears in the public catalog.
Email is delivered through Resend. Codes expire after 15 minutes, have a limited number of attempts and can only be consumed once. Entering the code confirms sign-in; email scanners cannot consume it by following a link.
What validation checks
The store checks account ownership, archive structure and limits, the manifest, component validity, supported imports and the filesystem interface. It never runs a publisher’s component during validation. Memona repeats package and digest validation when installing.
Automatic validation is not a source-code review. Email verification confirms control of an address; it does not certify a developer’s identity or behavior.
Releases and updates
Versions are immutable. Upload a new semantic version for a fix, even if the old version has been withdrawn. Users select and install releases inside Memona’s Plugins dialog; website links do not automatically install code.
Describe connection requirements and supported operations clearly. Include license information and a README in the package. Do not embed private service credentials, publisher sessions or verification tokens.
Keep API compatibility separate from your plugin’s release number. A new plugin release can continue using the same Memona WIT API. A component requiring an unsupported API cannot be installed until Memona supports that contract.
World filesystem
Memona filesystem extension ABI 0.1.0, distributed as a WebAssembly component. Rust guests can target wasm32-wasip2. Selected WASI 0.2 CLI, clocks, random, IO and filesystem interfaces are allowed for the standard library, with empty arguments/environment, empty preopens, and discarded stdio. WASI sockets and HTTP imports are not allowed; networking uses the host interface.
- Imports:
- interface
memona:filesystem/host@0.1.0
- interface
- Exports:
- interface
memona:filesystem/provider@0.1.0
- interface
Import interface memona:filesystem/host@0.1.0
Host services scoped to the current connection. No ambient filesystem, environment, sockets, credentials, or network permissions are inherited.
Types
enum host-error
Stable failures without backend messages or credential-bearing URLs.
Enum Cases
enum log-level
Severity of a diagnostic event. The host may discard message contents.
Enum Cases
record header
An HTTP header. Host-controlled transport headers cannot be overridden.
Record Fields
record request
HTTP(S) request, at most 4 MiB body and 64 headers of 8 KiB each.
Record Fields
method:stringurl:stringheaders: list<header>body: list<u8>
record response
Bounded HTTP response. Bodies exceeding 4 MiB fail; they are not truncated.
Record Fields
status:u16headers: list<header>body: list<u8>
Functions
http: func
Requests only hosts both declared by the package and granted on the connection. Each redirect and DNS result is checked, and DNS is pinned. Private/LAN addresses additionally require the private-network grant. Cross-origin redirects remove request headers; HTTPS cannot downgrade.
Params
request:request
Return values
- result<
response,host-error>
log: func
Emits a diagnostic event. Never include secrets or private document data.
Params
level:log-levelmessage:string
config: func
Reads configuration or a session-only secret for this connection.
Params
Return values
Export interface memona:filesystem/provider@0.1.0
Types
enum error-code
Stable operation failures. A mutation interrupted after dispatch can report outcome-unknown: inspect remote state before deciding to retry.
Enum Cases
enum entry-kind
Links and other backend object kinds are not exposed in API 0.1.
Enum Cases
record config-value
A configuration field. Boolean values are serialized as true or false.
Record Fields
record capabilities
Optional operations. False capabilities must return unsupported. Conditional-write promises atomic create-only and revision comparisons.
Record Fields
record file-info
Byte length and an opaque revision (maximum 4096 UTF-8 bytes), if known.
Record Fields
kind:entry-kindsize:u64revision: option<string>
record entry
One child name, without slashes, dot segments, or control characters.
Record Fields
name:stringinfo:file-info
record list-page
At most the requested number of entries. A cursor (maximum 4096 bytes) identifies another page; absence means enumeration is complete.
Record Fields
entries: list<entry>cursor: option<string>
record read-result
At most the requested byte count. EOF refers to this read’s ending offset.
Record Fields
variant write-condition
Atomic preconditions: absent file, exact opaque revision, or unconditional replacement. Providers must never simulate comparisons non-atomically.
Variant Cases
enum change-kind
Changes since a previous polling cursor.
Enum Cases
record change
A changed resource path relative to the connection root.
Record Fields
path:stringkind:change-kind
record change-page
At most 256 changes and a cursor of at most 4096 UTF-8 bytes.
Record Fields
changes: list<change>cursor: option<string>
Functions
open: func
Opens exactly one host-owned connection per component instance. The host passes only this connection’s configuration and in-memory credentials.
Params
provider-id:stringconfig: list<config-value>
Return values
- result<
capabilities,error-code>
close: func
Best-effort cleanup before disposal. The host may dispose immediately after a trap, cancellation, permission change, or process shutdown.
stat: func
Reads metadata without changing the resource.
Params
Return values
- result<
file-info,error-code>
list: func
Enumerates 1–256 direct children per request, without recursive traversal.
Params
Return values
- result<
list-page,error-code>
read: func
Reads a byte range, with length between 1 and 1,048,576. Larger files require repeated ranged reads; revisions let callers detect changes.
Params
Return values
- result<
read-result,error-code>
write: func
Replaces one complete file, at most 1,048,576 bytes. Oversized writes are rejected, never truncated. Streaming, append, and multipart writes are not supported in API 0.1. Never automatically retry a mutation.
Params
path:stringdata: list<u8>condition:write-condition
Return values
- result<
file-info,error-code>
mkdir: func
Creates one directory if mkdir capability is present.
Params
Return values
- result<_,
error-code>
rename: func
Renames within this connection if rename capability is present.
Params
Return values
- result<_,
error-code>
remove: func
Removes one file or empty directory if remove capability is present.
Params
Return values
- result<_,
error-code>
poll: func
Polls for a bounded change page if watch capability is present. No background callback or persistent subscription is installed by the host.
Params
Return values
- result<
change-page,error-code>