Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

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:

SettingWhat Memona’s agent file tool can do
OffThe connection is not available to agents.
Read onlyInspect folders and read files.
Read and writeRead 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:

ConditionBehavior
{"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.

FieldMeaning
schemaVersionManifest shape; currently 1.
idImmutable publisher/plugin identifier.
versionCanonical semantic release version.
apiVersionMemona plugin API version; currently 0.1.0.
entryMust be plugin.wasm in this format.
networkHostsDeclared network destinations; connection grants still apply.
providersStable 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

ItemLimit
Compressed package32 MiB
Expanded archive total128 MiB
WASM component64 MiB
Manifest64 KiB
README or license256 KiB each
PNG icon1 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

  1. Open plugin.memona.io and enter your email address.
  2. Enter the 16-character sign-in code from your email to create a publisher session.
  3. Claim a publisher name and prepare a package with that publisher in its ID.
  4. Upload your .memona-plugin package in the publisher dashboard.
  5. 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
  • Exports:
    • interface memona:filesystem/provider@0.1.0

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
  • denied
  • unavailable
  • invalid-input
  • cancelled
  • internal

enum log-level

Severity of a diagnostic event. The host may discard message contents.

Enum Cases
  • debug
  • info
  • warn
  • error

record header

An HTTP header. Host-controlled transport headers cannot be overridden.

Record Fields
  • name: string
  • value: string

record request

HTTP(S) request, at most 4 MiB body and 64 headers of 8 KiB each.

Record Fields
  • method: string
  • url: string
  • headers: list<header>
  • body: list<u8>

record response

Bounded HTTP response. Bodies exceeding 4 MiB fail; they are not truncated.

Record Fields
  • status: u16
  • headers: 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
Return values

log: func

Emits a diagnostic event. Never include secrets or private document data.

Params

config: func

Reads configuration or a session-only secret for this connection.

Params
  • key: string
Return values
  • option<string>

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
  • not-found
  • denied
  • unsupported
  • conflict
  • unavailable
  • invalid-input
  • cancelled
  • outcome-unknown
  • internal

enum entry-kind

Links and other backend object kinds are not exposed in API 0.1.

Enum Cases
  • file
  • directory

record config-value

A configuration field. Boolean values are serialized as true or false.

Record Fields
  • key: string
  • value: string

record capabilities

Optional operations. False capabilities must return unsupported. Conditional-write promises atomic create-only and revision comparisons.

Record Fields
  • write: bool
  • mkdir: bool
  • rename: bool
  • remove: bool
  • conditional-write: bool
  • watch: bool

record file-info

Byte length and an opaque revision (maximum 4096 UTF-8 bytes), if known.

Record Fields
  • kind: entry-kind
  • size: u64
  • revision: option<string>

record entry

One child name, without slashes, dot segments, or control characters.

Record Fields

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
  • data: list<u8>
  • revision: option<string>
  • eof: bool

variant write-condition

Atomic preconditions: absent file, exact opaque revision, or unconditional replacement. Providers must never simulate comparisons non-atomically.

Variant Cases
  • create-only
  • if-match: string
  • overwrite

enum change-kind

Changes since a previous polling cursor.

Enum Cases
  • created
  • modified
  • removed

record change

A changed resource path relative to the connection root.

Record Fields

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
Return values

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
  • path: string
Return values

list: func

Enumerates 1–256 direct children per request, without recursive traversal.

Params
  • path: string
  • cursor: option<string>
  • limit: u32
Return values

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
  • path: string
  • offset: u64
  • length: u32
Return values

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
Return values

mkdir: func

Creates one directory if mkdir capability is present.

Params
  • path: string
Return values

rename: func

Renames within this connection if rename capability is present.

Params
  • path: string
  • destination: string
Return values

remove: func

Removes one file or empty directory if remove capability is present.

Params
  • path: string
Return values

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
  • cursor: option<string>
Return values